package api import ( "encoding/json" "reflect" "strings" "github.com/aws/aws-lambda-go/events" "gitlab.com/uafrica/go-utils/errors" "gitlab.com/uafrica/go-utils/reflection" "gitlab.com/uafrica/go-utils/service" ) type Context interface { service.Context Request() events.APIGatewayProxyRequest GetRequestParams(paramsStructType reflect.Type) (interface{}, error) GetRequestBody(requestStructType reflect.Type) (interface{}, error) LogAPIRequestAndResponse(res events.APIGatewayProxyResponse, err error) } var contextInterfaceType = reflect.TypeOf((*Context)(nil)).Elem() type apiContext struct { service.Context request events.APIGatewayProxyRequest } func (ctx apiContext) Request() events.APIGatewayProxyRequest { return ctx.request } //todo: change to be a ctx method that defer to log so it does not have to be called explicitly //it should also capture metrics for the handler and automaticlaly write the audit record, //(but still allow for audit to be suppressed may be in some cases) func (ctx *apiContext) LogAPIRequestAndResponse(res events.APIGatewayProxyResponse, err error) { fields := map[string]interface{}{ "path": ctx.request.Path, "method": ctx.request.HTTPMethod, "status_code": res.StatusCode, "api_gateway_request_id": ctx.RequestID(), } if ctx.request.HTTPMethod == "GET" { fields["req-query"] = ctx.request.QueryStringParameters } statusOK := res.StatusCode >= 200 && res.StatusCode <= 299 if err != nil || !statusOK { fields["error"] = err fields["req-body"] = ctx.request.Body fields["req-query"] = ctx.request.QueryStringParameters fields["res-body"] = res.Body } ctx.Context.WithFields(fields).Infof("Request & Response: err=%+v", err) } //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 { return nil, errors.Wrapf(err, "failed to put query param values into struct") } if err := ctx.applyClaim("params", paramsStructValuePtr.Interface()); err != nil { return nil, errors.Wrapf(err, "failed to fill claims on params") } if validator, ok := paramsStructValuePtr.Interface().(IValidator); ok { if err := validator.Validate(); err != nil { return nil, errors.Wrapf(err, "invalid params") } } return paramsStructValuePtr.Elem().Interface(), nil } func (ctx apiContext) extract(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) } continue default: } n := (strings.SplitN(f.Tag.Get("json"), ",", 2))[0] if n == "" { n = strings.ToLower(f.Name) } if n == "" || n == "-" { continue } //get value(s) from query string var paramStrValues []string if paramStrValue, isDefined := ctx.request.QueryStringParameters[n]; isDefined { if len(paramStrValue) >= 2 && paramStrValue[0] == '[' && paramStrValue[len(paramStrValue)-1] == ']' { paramStrValues = strings.Split(paramStrValue[1:len(paramStrValue)-1], ",") //from [CSV] } else { paramStrValues = []string{paramStrValue} //single value } } else { paramStrValues = ctx.request.MultiValueQueryStringParameters[n] } if len(paramStrValues) == 0 { continue //param has no value specified in URL } //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]) } v.Field(i).Set(reflect.Append(v.Field(i), newValuePtr.Elem())) } } else { if len(paramStrValues) > 1 { return errors.Errorf("%s does not support >1 values(%v)", n, strings.Join(paramStrValues, ",")) } //single value specified if err := reflection.SetValue(v.Field(i), paramStrValues[0]); err != nil { return errors.Wrapf(err, "failed to set %s=%s", n, paramStrValues[0]) } } } //for each param struct field return nil } func (ctx apiContext) GetRequestBody(requestStructType reflect.Type) (interface{}, error) { requestStructValuePtr := reflect.New(requestStructType) err := json.Unmarshal([]byte(ctx.request.Body), requestStructValuePtr.Interface()) if err != nil { return nil, errors.Wrapf(err, "failed to JSON request body") } if err := ctx.applyClaim("body", requestStructValuePtr.Interface()); err != nil { return nil, errors.Wrapf(err, "failed to fill claims on body") } if validator, ok := requestStructValuePtr.Interface().(IValidator); ok { if err := validator.Validate(); err != nil { return nil, errors.Wrapf(err, "invalid request body") } } return requestStructValuePtr.Elem().Interface(), nil } type IValidator interface { Validate() error } func (ctx *apiContext) applyClaim(name string, valuePtr interface{}) error { t := reflect.TypeOf(valuePtr) if t.Kind() != reflect.Ptr { return errors.Errorf("%T is not a pointer", valuePtr) //programming error... it must be a pointer to be able to change it } t = t.Elem() if t.Kind() != reflect.Struct { //ctx.Debugf("Not setting claims on %T", valuePtr) return nil //not a struct - nothing to do - is allowed e.g. for posting a string. } if err := ctx.setClaim(name, t, reflect.ValueOf(valuePtr).Elem()); err != nil { return errors.Wrapf(err, "failed to set claim on %T", valuePtr) } return nil } func (ctx *apiContext) setClaim(name string, structType reflect.Type, structValue reflect.Value) error { if len(ctx.Claim()) == 0 { ctx.Debugf("NO CLAIM to apply to %s of type (%s)", name, structType.Name()) return nil } for fieldName, claimValue := range ctx.Claim() { if field := structValue.FieldByName(fieldName); field.IsValid() { if err := reflection.SetValue(field, claimValue); err != nil { return errors.Errorf("failed to set %s.%s=(%T)%v", structType.Name(), fieldName, claimValue, claimValue) } ctx.Debugf("defined claim %s.%s=(%T)%v ...", name, fieldName, claimValue, claimValue) // } else { // ctx.Debugf("claim(%s) does not apply to %s", fieldName, structType.Name()) } } //recurse into sub-structs and sub struct ptrs (not yet slices) for i := 0; i < structType.NumField(); i++ { f := structType.Field(i) if len(f.Name) > 0 && f.Name[0] >= 'a' && f.Name[0] <= 'z' { //private field - do not enter continue } if f.Type.Kind() == reflect.Struct { if err := ctx.setClaim(name+"."+structType.Field(i).Name, f.Type, structValue.Field(i)); err != nil { return errors.Wrapf(err, "failed to set claim on sub struct %s.%s", structType.Name(), f.Name) } } if f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && !structValue.Field(i).IsNil() { if err := ctx.setClaim(name+"."+structType.Field(i).Name, f.Type.Elem(), structValue.Field(i).Elem()); err != nil { return errors.Wrapf(err, "failed to set claim on sub &struct %s.%s", structType.Name(), f.Name) } } } return nil }