Skip to content
Snippets Groups Projects
Commit 93423230 authored by Jan Semmelink's avatar Jan Semmelink
Browse files

Update api URL param parsing to support mostly any type of value used in param struct

parent 756e8157
No related branches found
No related tags found
1 merge request!6Search package improvements to retrieve documents with text searches from OpenSearch
......@@ -58,13 +58,8 @@ func (ctx *apiContext) LogAPIRequestAndResponse(res events.APIGatewayProxyRespon
//allocate struct for params, populate it from the URL parameters then validate and return the struct
func (ctx apiContext) GetRequestParams(paramsStructType reflect.Type) (interface{}, error) {
paramValues := map[string]interface{}{}
for n, v := range ctx.request.QueryStringParameters {
paramValues[n] = v
}
paramsStructValuePtr := reflect.New(paramsStructType)
if err := ctx.extract("params", paramsStructType, paramsStructValuePtr.Elem()); err != nil {
if err := ctx.setParamsInStruct("params", paramsStructType, paramsStructValuePtr.Elem()); err != nil {
return nil, errors.Wrapf(err, "failed to put query param values into struct")
}
if err := ctx.applyClaim("params", paramsStructValuePtr.Interface()); err != nil {
......@@ -78,30 +73,37 @@ func (ctx apiContext) GetRequestParams(paramsStructType reflect.Type) (interface
return paramsStructValuePtr.Elem().Interface(), nil
}
func (ctx apiContext) extract(name string, t reflect.Type, v reflect.Value) error {
//extract params into a struct value
func (ctx apiContext) setParamsInStruct(name string, t reflect.Type, v reflect.Value) error {
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
switch f.Type.Kind() {
case reflect.Struct:
if err := ctx.extract(name+"."+f.Name, t.Field(i).Type, v.Field(i)); err != nil {
return errors.Wrapf(err, "failed to fill sub %s.%s", name, f.Name)
tf := t.Field(i)
//enter into anonymous sub-structs
if tf.Anonymous {
if tf.Type.Kind() == reflect.Struct {
if err := ctx.setParamsInStruct(name+"."+tf.Name, t.Field(i).Type, v.Field(i)); err != nil {
return errors.Wrapf(err, "failed on parameters %s.%s", name, tf.Name)
}
continue
default:
}
return errors.Errorf("parameters cannot parse into anonymous %s field %s", tf.Type.Kind(), tf.Type.Name())
}
n := (strings.SplitN(f.Tag.Get("json"), ",", 2))[0]
//named field:
//use name from json tag, else lowercase of field name
n := (strings.SplitN(tf.Tag.Get("json"), ",", 2))[0]
if n == "" {
n = strings.ToLower(f.Name)
n = strings.ToLower(tf.Name)
}
if n == "" || n == "-" {
continue
continue //skip fields without name
}
//get value(s) from query string
//see if this named param was specified
var paramStrValues []string
if paramStrValue, isDefined := ctx.request.QueryStringParameters[n]; isDefined {
//specified once in URL
if len(paramStrValue) >= 2 && paramStrValue[0] == '[' && paramStrValue[len(paramStrValue)-1] == ']' {
//specified as CSV inside [...] e.g. id=[1,2,3]
csvReader := csv.NewReader(strings.NewReader(paramStrValue[1 : len(paramStrValue)-1]))
var err error
paramStrValues, err = csvReader.Read()
......@@ -109,43 +111,58 @@ func (ctx apiContext) extract(name string, t reflect.Type, v reflect.Value) erro
return errors.Wrapf(err, "invalid CSV: [%s]", paramStrValue)
}
} else {
paramStrValues = []string{paramStrValue} //single value
//specified as single value only e.g. id=1
paramStrValues = []string{paramStrValue}
}
} else {
//specified multiple times e.g. id=1&id=2&id=3
paramStrValues = ctx.request.MultiValueQueryStringParameters[n]
}
if len(paramStrValues) == 0 {
continue //param has no value specified in URL
}
valueField := v.Field(i)
if valueField.Kind() == reflect.Ptr {
valueField.Set(reflect.New(valueField.Type().Elem()))
valueField = valueField.Elem()
}
//param is defined >=1 times in URL
if f.Type.Kind() == reflect.Slice {
//iterate over all specified values
for index, paramStrValue := range paramStrValues {
newValuePtr := reflect.New(f.Type.Elem())
if err := reflection.SetValue(newValuePtr.Elem(), paramStrValue); err != nil {
return errors.Wrapf(err, "failed to set %s[%d]=%s", n, index, paramStrValues[0])
if tf.Type.Kind() == reflect.Slice {
//this param struct field is a slice, iterate over all specified values
for i, paramStrValue := range paramStrValues {
paramValue, err := parseParamValue(paramStrValue, tf.Type.Elem())
if err != nil {
return errors.Wrapf(err, "invalid %s[%d]", n, i)
}
v.Field(i).Set(reflect.Append(v.Field(i), newValuePtr.Elem()))
valueField.Set(reflect.Append(valueField, paramValue))
}
} else {
if len(paramStrValues) > 1 {
return errors.Errorf("%s does not support >1 values(%v)", n, strings.Join(paramStrValues, ","))
return errors.Errorf("parameter %s does not support multiple values [%s]", n, strings.Join(paramStrValues, ","))
}
//single value specified
valueField := v.Field(i)
if valueField.Kind() == reflect.Ptr {
valueField.Set(reflect.New(valueField.Type().Elem()))
valueField = valueField.Elem()
}
if err := reflection.SetValue(valueField, paramStrValues[0]); err != nil {
return errors.Wrapf(err, "failed to set %s=%s", n, paramStrValues[0])
paramValue, err := parseParamValue(paramStrValues[0], valueField.Type())
if err != nil {
return errors.Wrapf(err, "invalid %s", n)
}
valueField.Set(paramValue)
}
} //for each param struct field
return nil
}
func parseParamValue(s string, t reflect.Type) (reflect.Value, error) {
newValuePtr := reflect.New(t)
if err := json.Unmarshal([]byte("\""+s+"\""), newValuePtr.Interface()); err != nil {
if err := json.Unmarshal([]byte(s), newValuePtr.Interface()); err != nil {
return newValuePtr.Elem(), errors.Wrapf(err, "invalid \"%s\"", s)
}
}
return newValuePtr.Elem(), nil
}
func (ctx apiContext) GetRequestBody(requestStructType reflect.Type) (interface{}, error) {
requestStructValuePtr := reflect.New(requestStructType)
err := json.Unmarshal([]byte(ctx.request.Body), requestStructValuePtr.Interface())
......
......@@ -2,8 +2,10 @@ package api_test
import (
"context"
"encoding/json"
"reflect"
"testing"
"time"
"github.com/aws/aws-lambda-go/events"
"gitlab.com/uafrica/go-utils/api"
......@@ -34,6 +36,7 @@ func TestNested(t *testing.T) {
ctx, err = api.New("request-id", nil).NewContext(
context.Background(),
"123",
//all URL params are specified as string values
events.APIGatewayProxyRequest{
QueryStringParameters: map[string]string{
"a": "1", //must be written into P3.P2.P1.A
......@@ -47,11 +50,11 @@ func TestNested(t *testing.T) {
},
})
if err != nil {
t.Fatal(err)
t.Fatalf("ERROR: %+v", err)
}
if p3d, err := ctx.GetRequestParams(reflect.TypeOf(P3{})); err != nil {
t.Fatal(err)
t.Fatalf("ERROR: %+v", err)
} else {
p3 := p3d.(P3)
t.Logf("p3: %+v", p3)
......@@ -67,6 +70,77 @@ func TestNested(t *testing.T) {
}
}
type ParamTypes struct {
GetParams
Nr int64 `json:"nr"`
Name string `json:"name"`
NrOpt *int64 `json:"nr_opt"`
NameOpt *string `json:"name_opt"`
Time1 time.Time `json:"time1"`
Time2 *time.Time `json:"time2"`
Dur1 time.Duration `json:"dur1"`
Dur2 *time.Duration `json:"dur2"`
//lists of values
NrList []int64 `json:"nrs"`
NameList []string `json:"names"`
NrOptList []*int64 `json:"nrs_opt"`
NameOptList []*string `json:"names_opt"`
Time1List []time.Time `json:"time1s"`
Time2List []*time.Time `json:"time2s"`
Dur1List []time.Duration `json:"dur1s"`
Dur2List []*time.Duration `json:"dur2s"`
}
func TestTypes(t *testing.T) {
logger.SetGlobalLevel(logger.LevelDebug)
logger.SetGlobalFormat(logger.NewConsole())
var ctx api.Context
var err error
ctx, err = api.New("request-id", nil).NewContext(
context.Background(),
"123",
//all URL params are specified as string values
events.APIGatewayProxyRequest{
QueryStringParameters: map[string]string{
"nr": "1",
"name": "name2",
"nr_opt": "3",
"name_opt": "name4",
"limit": "5",
"time1": "2021-11-23T00:00:00+00:00",
"time2": "2021-11-23T00:00:00+00:00",
"dur1": "4", //nanoseconds
"dur2": "4", //nanoseconds
"nrs": "[1,2,3]",
"nrs_opt": "[4,5,6]",
"names": "[A,B,C]",
"names_opt": "[D,E,F]",
"time1s": "[2021-11-23T00:00:00+00:00]",
"dur1s": "[4,5,6]", //nanoseconds
},
MultiValueQueryStringParameters: map[string][]string{
"dur2s": {"11", "12", "13"},
"time2s": {"2021-11-23T00:00:00+00:00", "2021-11-23T00:00:00+00:00", "2021-11-23T00:00:00+00:00"},
},
})
if err != nil {
t.Fatalf("ERROR: %+v", err)
}
if pd, err := ctx.GetRequestParams(reflect.TypeOf(ParamTypes{})); err != nil {
t.Fatalf("ERROR: %+v", err)
} else {
p := pd.(ParamTypes)
t.Logf("p: %+v", p)
if p.Nr != 1 || p.Name != "name2" || p.NrOpt == nil || *p.NrOpt != 3 || p.NameOpt == nil || *p.NameOpt != "name4" || p.Limit != 5 {
t.Errorf("Wrong values: %+v", p)
}
jsonParams, _ := json.Marshal(p)
t.Logf("params: %s", string(jsonParams))
}
}
type PageParams struct {
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
......@@ -94,6 +168,7 @@ func TestGet(t *testing.T) {
ctx, err = api.New("request-id", nil).NewContext(
context.Background(),
"123",
//all URL params are specified as string values
events.APIGatewayProxyRequest{
QueryStringParameters: map[string]string{
"id": "1",
......@@ -109,11 +184,11 @@ func TestGet(t *testing.T) {
},
})
if err != nil {
t.Fatal(err)
t.Fatalf("ERROR: %+v", err)
}
if p3d, err := ctx.GetRequestParams(reflect.TypeOf(MyGetParams{})); err != nil {
t.Fatal(err)
t.Fatalf("ERROR: %+v", err)
} else {
get := p3d.(MyGetParams)
t.Logf("get: %+v", get)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment