[Question] Custom slice/array of integers decoder
krispetkov opened this issue ยท 6 comments
Hey @ggicci,
First of all amazing package, really useful thing ๐
I have a question related to how can I decode a query array of integers. I want something like this: http://url.com?id=1,2,3
and I want this to be parsed as []int
property in my struct.
I saw in the docs that there is a possibility with using ?id=1&id=2...
but for me it's not really convinient when you can just set them all with comma between them.
I wanted to ask what is should I use to create something like a custom decoder in that case? The decoder/coder or custom directive?
Thanks!
Actually I tried to modify the provided example in custom directive, but it seems to not be working at all. The functions are not being called at all ๐ค I don't know if it has something to do that I'm using:
httpIn "github.com/ggicci/httpin"
if err = httpIn.Decode(r, &filters); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
And here what implementation I tried
type DirectiveCaseFormatter struct {
Transform func(string) []int
}
func (f *DirectiveCaseFormatter) Decode(rtm *httpInCore.DirectiveRuntime) error {
if rtm.Value.Type().Elem().Kind() != reflect.String {
return errors.New("not a string")
}
currentValue := rtm.Value.Elem().String()
newValue := f.Transform(currentValue)
slice := reflect.MakeSlice(reflect.TypeOf([]int{}), len(newValue), len(newValue))
for i, num := range newValue {
slice.Index(i).SetInt(int64(num))
}
rtm.Value.Elem().Set(slice)
return nil
}
func (f *DirectiveCaseFormatter) Encode(rtm *httpInCore.DirectiveRuntime) error {
if rtm.Value.Type().Kind() != reflect.String {
return errors.New("not a string")
}
currentValue := rtm.Value.String()
newValue := f.Transform(currentValue)
slice := reflect.MakeSlice(reflect.TypeOf([]int{}), len(newValue), len(newValue))
for i, num := range newValue {
slice.Index(i).SetInt(int64(num))
}
rtm.Value.Elem().Set(slice)
return nil
}
httpInCore.RegisterDirective("to_array", &DirectiveCaseFormatter{
Transform: func(s string) []int {
pieces := strings.Split(s, ",")
ints := make([]int, len(pieces))
for i, piece := range pieces {
num, err := strconv.Atoi(piece)
if err != nil {
log.Printf("Could not convert piece to integer: %v", err)
continue
}
ints[i] = num
}
return ints
},
})
Hi @krispetkov, currently httpin doesn't support your use case. In httpin, a value of a single key in the request (id=1,2,3
) is treated as a scalar value and won't be converted/mapped to an array. i.e.
?id=1,2,3 ---> only can be mapped to type T(string, int, time.Time, MyDate, etc),
but not []T ([]int, []string, etc)
While there's a tricky apprach, let's create a custom type and implement the Stringable
interface:
Online demo: https://go.dev/play/p/4IiRYr8n7zg
type CommaSeparatedIntegerArray struct {
Values []int
}
func (a CommaSeparatedIntegerArray) ToString() (string, error) {
var res = make([]string, len(a.Values))
for i := range a.Values {
res[i] = strconv.Itoa(a.Values[i])
}
return strings.Join(res, ","), nil
}
func (pa *CommaSeparatedIntegerArray) FromString(value string) error {
a := CommaSeparatedIntegerArray{}
values := strings.Split(value, ",")
a.Values = make([]int, len(values))
for i := range values {
if value, err := strconv.Atoi(values[i]); err != nil {
return err
} else {
a.Values[i] = value
}
}
*pa = a
return nil
}
type ListUsersInput struct {
IdList CommaSeparatedIntegerArray `in:"query=id"`
}
Hey @ggicci, thanks for the proposed solution. I also came up with something, but by using the custom directive. It's just a sample and can be further improved (the logic, the usage of hardcoded ids name, etc.), but I think something like this may also do the work:
import (
httpInCore "github.com/ggicci/httpin/core"
httpInIntegration "github.com/ggicci/httpin/integration"
)
func init() {
httpInCore.RegisterDirective("toArray", &ToArrayFormatter{})
}
type ToArrayFormatter struct {
}
func (f *ToArrayFormatter) Decode(rtm *httpInCore.DirectiveRuntime) error {
req := rtm.GetRequest()
params, err := url.ParseQuery(req.URL.RawQuery)
if err != nil {
return fmt.Errorf("could not parse query: %v", err)
}
if len(params["ids"]) == 1 { // it was passed as a list of ids (eg. ?ids=1,2,3)
ids, ok := params["ids"]
if ok {
// Try to split the string by comma
idStrings := strings.Split(ids[0], ",")
if len(idStrings) == 0 {
// Try to split the string by semicolon
idStrings = strings.Split(ids[0], ";")
}
params.Del("ids")
for _, id := range idStrings {
params.Add("ids", id)
}
}
req.URL.RawQuery = params.Encode()
} else {
// Check if any of the passed ids is a valid number
for _, id := range params["ids"] {
_, err := strconv.ParseFloat(id, 64)
if err != nil {
return fmt.Errorf("arg %s is not a valid number", id)
}
}
}
return nil
}
func (f *ToArrayFormatter) Encode(rtm *httpInCore.DirectiveRuntime) error {
// Your code here ...
return nil
}
type Filters struct {
Ids []int `in:"toArray;query=ids"` // query param (eg. ?ids=1,2,3)
}
@krispetkov your idea looks good to me. I think rewriting the query from ids=1,2,3
to ids=1&ids=2&ids=3
is a better way for this case. As httpin does support parsing the latter format of query. And you can use type []int
as well.
Thanks for the help and the info! I will close the issue, and I hope it will help someone in future ๐