package api import ( "context" "encoding/json" "fmt" "reflect" "strconv" "strings" "time" "github.com/aws/aws-lambda-go/events" "github.com/uptrace/bun" "gitlab.com/uafrica/go-utils/errors" "gitlab.com/uafrica/go-utils/logger" "gitlab.com/uafrica/go-utils/queues" "gitlab.com/uafrica/go-utils/service" ) type IContext interface { context.Context logger.ILogger queues.IProducer StartTime() time.Time MillisecondsSinceStart() int64 CheckValues(checkName string) interface{} CheckValue(checkName, valueName string) interface{} } type Context struct { service.Context queues.IProducer Request events.APIGatewayProxyRequest RequestID string ValuesFromChecks map[string]map[string]interface{} //also in context.Value(), but cannot retrieve iteratively from there for logging... DB *bun.DB } func (ctx Context) CheckValues(checkName string) interface{} { if cv, ok := ctx.ValuesFromChecks[checkName]; ok { return cv } return nil } func (ctx Context) CheckValue(checkName, valueName string) interface{} { if cv, ok := ctx.ValuesFromChecks[checkName]; ok { if v, ok := cv[valueName]; ok { return v } } return nil } // func (ctx Context) Audit(org, new interface{}, eventType types.AuditEventType) { // //call old function for now - should become part of context ONLY // audit.SaveAuditEvent(org, new, ctx.Claims, eventType, &ctx.RequestID) // } //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 Context) 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 for checkName, checkValues := range ctx.ValuesFromChecks { for name, value := range checkValues { fields[checkName+"_"+name] = value } } } ctx.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 Context) GetRequestParams(paramsStructType reflect.Type) (interface{}, error) { paramValues := map[string]interface{}{} for n, v := range ctx.Request.QueryStringParameters { paramValues[n] = v } paramsStructValuePtr := reflect.New(paramsStructType) for i := 0; i < paramsStructType.NumField(); i++ { f := paramsStructType.Field(i) 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 { 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 := setParamFromStr(fmt.Sprintf("%s[%d]", n, index), //paramsStructValuePtr.Elem().Field(i).Index(index), newValuePtr.Elem(), //.Elem() to dereference paramStrValue); err != nil { return nil, errors.Wrapf(err, "failed to set %s[%d]=%s", n, i, paramStrValues[0]) } paramsStructValuePtr.Elem().Field(i).Set(reflect.Append(paramsStructValuePtr.Elem().Field(i), newValuePtr.Elem())) } } else { if len(paramStrValues) > 1 { return nil, errors.Errorf("%s does not support >1 values(%v)", n, strings.Join(paramStrValues, ",")) } //single value specified if err := setParamFromStr(n, paramsStructValuePtr.Elem().Field(i), paramStrValues[0]); err != nil { return nil, errors.Wrapf(err, "failed to set %s=%s", n, paramStrValues[0]) } } } //for each param struct field 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 setParamFromStr(fieldName string, fieldValue reflect.Value, paramStrValue string) error { logger.Debugf("Set(%s,%v,%v,%v)", fieldName, fieldValue.Type(), fieldValue.Kind(), paramStrValue) switch fieldValue.Type().Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: //parse to int for this struct field if i64, err := strconv.ParseInt(paramStrValue, 10, 64); err != nil { return errors.Errorf("%s is not a number", paramStrValue) } else { switch fieldValue.Type().Kind() { case reflect.Int: fieldValue.Set(reflect.ValueOf(int(i64))) case reflect.Int8: fieldValue.Set(reflect.ValueOf(int8(i64))) case reflect.Int16: fieldValue.Set(reflect.ValueOf(int16(i64))) case reflect.Int32: fieldValue.Set(reflect.ValueOf(int32(i64))) case reflect.Int64: fieldValue.Set(reflect.ValueOf(i64)) } } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: //parse to int for this struct field if u64, err := strconv.ParseUint(paramStrValue, 10, 64); err != nil { return errors.Errorf("%s is not a number", paramStrValue) } else { switch fieldValue.Type().Kind() { case reflect.Uint: fieldValue.Set(reflect.ValueOf(uint(u64))) case reflect.Uint8: fieldValue.Set(reflect.ValueOf(uint8(u64))) case reflect.Uint16: fieldValue.Set(reflect.ValueOf(uint16(u64))) case reflect.Uint32: fieldValue.Set(reflect.ValueOf(uint32(u64))) case reflect.Uint64: fieldValue.Set(reflect.ValueOf(u64)) } } case reflect.Bool: bs := strings.ToLower(paramStrValue) if bs == "true" || bs == "yes" || bs == "1" { fieldValue.Set(reflect.ValueOf(true)) } case reflect.String: fieldValue.Set(reflect.ValueOf(paramStrValue)) case reflect.Float32: if f32, err := strconv.ParseFloat(paramStrValue, 32); err != nil { return errors.Wrapf(err, "invalid float") } else { fieldValue.Set(reflect.ValueOf(float32(f32))) } case reflect.Float64: if f64, err := strconv.ParseFloat(paramStrValue, 64); err != nil { return errors.Wrapf(err, "invalid float") } else { fieldValue.Set(reflect.ValueOf(f64)) } default: return errors.Errorf("unsupported type %v", fieldValue.Type().Kind()) } //switch param struct field return nil } func (ctx Context) 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 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 }