Skip to content

Commit

Permalink
feat(tdhttp): add tdhttp.Q type to easily declare query parameters
Browse files Browse the repository at this point in the history
Closes #165.

Signed-off-by: Maxime Soulé <[email protected]>
  • Loading branch information
maxatome committed Aug 31, 2021
1 parent 77556b1 commit 91cb46e
Show file tree
Hide file tree
Showing 6 changed files with 569 additions and 185 deletions.
127 changes: 127 additions & 0 deletions helpers/tdhttp/q.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) 2021, Maxime Soulé
// All rights reserved.
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.

package tdhttp

import (
"errors"
"fmt"
"net/url"
"reflect"
"strconv"

"github.com/maxatome/go-testdeep/internal/color"
)

// Q allows to easily declare query parameters for use in tdhttp.NewRequest
// and related net/http.Request builders, as tdhttp.Get for example:
//
// req := tdhttp.Get("/path", tdhttp.Q{
// "id": []int64{1234, 4567},
// "dryrun": true,
// })
//
// See tdhttp.NewRequest documentation for several examples of use.
//
// Accepted types as values are:
// - fmt.Stringer
// - string
// - int, int8, int16, int32, int64
// - uint, uint8, uint16, uint32, uint64
// - float32, float64
// - bool
// - slice or array of any type above, plus interface{}
// - pointer on any type above, plus interface{} or any other pointer
type Q map[string]interface{}

// AddTo adds the q contents to qp.
func (q Q) AddTo(qp url.Values) error {
for param, value := range q {
// Ignore nil values
if value == nil {
continue
}
err := q.addParamTo(param, reflect.ValueOf(value), true, qp)
if err != nil {
return err
}
}
return nil
}

// Values returns a url.Values instance corresponding to q. It panics
// if a value cannot be converted.
func (q Q) Values() url.Values {
qp := make(url.Values, len(q))
err := q.AddTo(qp)
if err != nil {
panic(errors.New(color.Bad(err.Error())))
}
return qp
}

// Encode does the same as url.Values.Encode does. So quoting its doc,
// it encodes the values into “URL encoded” form ("bar=baz&foo=quux")
// sorted by key.
//
// It panics if a value cannot be converted.
func (q Q) Encode() string {
return q.Values().Encode()
}

func (q Q) addParamTo(param string, v reflect.Value, allowArray bool, qp url.Values) error {
var str string
for {
if s, ok := v.Interface().(fmt.Stringer); ok {
qp.Add(param, s.String())
return nil
}

switch v.Kind() {
case reflect.Slice, reflect.Array:
if !allowArray {
return fmt.Errorf("%s is only allowed at the root level for param %q",
v.Kind(), param)
}
for i, l := 0, v.Len(); i < l; i++ {
err := q.addParamTo(param, v.Index(i), false, qp)
if err != nil {
return err
}
}
return nil

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
str = strconv.FormatInt(v.Int(), 10)

case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
str = strconv.FormatUint(v.Uint(), 10)

case reflect.Float32, reflect.Float64:
str = strconv.FormatFloat(v.Float(), 'g', -1, 64)

case reflect.String:
str = v.String()

case reflect.Bool:
str = strconv.FormatBool(v.Bool())

case reflect.Ptr, reflect.Interface:
if !v.IsNil() {
v = v.Elem()
continue
}
return nil // mimic url.Values behavior ⇒ ignore

default:
return fmt.Errorf("don't know how to add type %s (%s) to param %q",
v.Type(), v.Kind(), param)
}

qp.Add(param, str)
return nil
}
}
150 changes: 150 additions & 0 deletions helpers/tdhttp/q_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright (c) 2021, Maxime Soulé
// All rights reserved.
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.

package tdhttp_test

import (
"net/url"
"testing"

"github.com/maxatome/go-testdeep/helpers/tdhttp"
"github.com/maxatome/go-testdeep/td"
)

type qTest1 struct{}

func (qTest1) String() string { return "qTest1!" }

type qTest2 struct{}

func (*qTest2) String() string { return "qTest2!" }

func TestQ(t *testing.T) {
q := tdhttp.Q{
"str1": "v1",
"str2": []string{"v20", "v21"},
"int1": 1234,
"int2": []int{1, 2, 3},
"uint1": uint(1234),
"uint2": [3]uint{1, 2, 3},
"float1": 1.2,
"float2": []float64{1.2, 3.4},
"bool1": true,
"bool2": [2]bool{true, false},
}
td.Cmp(t, q.Values(), url.Values{
"str1": []string{"v1"},
"str2": []string{"v20", "v21"},
"int1": []string{"1234"},
"int2": []string{"1", "2", "3"},
"uint1": []string{"1234"},
"uint2": []string{"1", "2", "3"},
"float1": []string{"1.2"},
"float2": []string{"1.2", "3.4"},
"bool1": []string{"true"},
"bool2": []string{"true", "false"},
})

// Auto deref pointers
num := 123
pnum := &num
ppnum := &pnum
q = tdhttp.Q{
"pnum": pnum,
"ppnum": ppnum,
"pppnum": &ppnum,
"slice": []***int{&ppnum, &ppnum},
"pslice": &[]***int{&ppnum, &ppnum},
"array": [2]***int{&ppnum, &ppnum},
"parray": &[2]***int{&ppnum, &ppnum},
}
td.Cmp(t, q.Values(), url.Values{
"pnum": []string{"123"},
"ppnum": []string{"123"},
"pppnum": []string{"123"},
"slice": []string{"123", "123"},
"pslice": []string{"123", "123"},
"array": []string{"123", "123"},
"parray": []string{"123", "123"},
})

// Auto deref interfaces
q = tdhttp.Q{
"all": []interface{}{
"string",
-1,
int8(-2),
int16(-3),
int32(-4),
int64(-5),
uint(1),
uint8(2),
uint16(3),
uint32(4),
uint64(5),
float32(6),
float64(7),
true,
ppnum,
(*int)(nil), // ignored
nil, // ignored
qTest1{},
&qTest1{}, // does not implement fmt.Stringer, but qTest does
// qTest2{} panics as it does not implement fmt.Stringer, see Errors below
&qTest2{},
},
}
td.Cmp(t, q.Values(), url.Values{
"all": []string{
"string",
"-1",
"-2",
"-3",
"-4",
"-5",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"true",
"123",
"qTest1!",
"qTest1!",
"qTest2!",
},
})

// nil case
pnum = nil
q = tdhttp.Q{
"nil1": &ppnum,
"nil2": (*int)(nil),
"nil3": nil,
"nil4": []*int{nil, nil},
"nil5": ([]int)(nil),
"nil6": []interface{}{nil, nil},
}
td.Cmp(t, q.Values(), url.Values{})

q = tdhttp.Q{
"id": []int{12, 34},
"draft": true,
}
td.Cmp(t, q.Encode(), "draft=true&id=12&id=34")

// Errors
td.CmpPanic(t, func() { (tdhttp.Q{"panic": map[string]bool{}}).Values() },
td.Contains(`don't know how to add type map[string]bool (map) to param "panic"`))
td.CmpPanic(t, func() { (tdhttp.Q{"panic": qTest2{}}).Values() },
td.Contains(`don't know how to add type tdhttp_test.qTest2 (struct) to param "panic"`))

td.CmpPanic(t,
func() { (tdhttp.Q{"panic": []interface{}{[]int{}}}).Values() },
td.Contains(`slice is only allowed at the root level for param "panic"`))
}
Loading

0 comments on commit 91cb46e

Please sign in to comment.