Skip to content
Snippets Groups Projects
Commit d81b543f authored by Cornel Rautenbach's avatar Cornel Rautenbach
Browse files

Global context

parent 54e1dee7
No related branches found
No related tags found
No related merge requests found
......@@ -14,9 +14,8 @@ import (
"gitlab.com/uafrica/go-utils/string_utils"
)
//LEGACY: global variable is set only for backward compatibility
//When handlers are changed to accept context, they should get this from the context
var CurrentRequestID *string
// Ctx extends service ctx to include url etc.
var Ctx Context
//New creates the API with the specified routes keys on [path][method]
//value could be any of the handler function signatures supported by the api.Router
......
......@@ -17,38 +17,30 @@ func NewHandler(fnc interface{}) (handler, error) {
h := handler{}
fncType := reflect.TypeOf(fnc)
if fncType.NumIn() < 2 || fncType.NumIn() > 3 {
return h, errors.Errorf("takes %d args instead of (Context, Params[, Body])", fncType.NumIn())
if fncType.NumIn() < 1 || fncType.NumIn() > 2 {
return h, errors.Errorf("takes %d args instead of (Params[, Body])", fncType.NumIn())
}
if fncType.NumOut() < 1 || fncType.NumOut() > 2 {
return h, errors.Errorf("returns %d results instead of ([Response,] error)", fncType.NumOut())
}
//arg[0] must implement interface api.Context
//if _, ok := reflect.New(fncType.In(0)).Interface().(Context); !ok {
if fncType.In(0) != contextInterfaceType &&
!fncType.In(0).Implements(contextInterfaceType) {
return h, errors.Errorf("first arg %v does not implement %v", fncType.In(0), contextInterfaceType)
}
//arg[1] must be a struct for params. It may be an empty struct, but
//arg[0] must be a struct for params. It may be an empty struct, but
//all public fields require a json tag which we will use to math the URL param name
if err := validateStructType(fncType.In(1)); err != nil {
return h, errors.Wrapf(err, "second arg %v is not valid params struct type", fncType.In(1))
if err := validateStructType(fncType.In(0)); err != nil {
return h, errors.Wrapf(err, "second arg %v is not valid params struct type", fncType.In(0))
}
h.RequestParamsType = fncType.In(1)
h.RequestParamsType = fncType.In(0)
//arg[2] is optional and must be a struct for request body. It may be an empty struct, but
//arg[1] is optional and must be a struct for request body. It may be an empty struct, but
//all public fields require a json tag which we will use to unmarshal the request body from JSON
if fncType.NumIn() >= 3 {
if fncType.In(2).Kind() == reflect.Slice {
if err := validateStructType(fncType.In(2).Elem()); err != nil {
return h, errors.Errorf("third arg %v is not valid body []struct type", fncType.In(2))
if fncType.NumIn() >= 2 {
if fncType.In(1).Kind() == reflect.Slice {
if err := validateStructType(fncType.In(1).Elem()); err != nil {
return h, errors.Errorf("second arg %v is not valid body []struct type", fncType.In(1))
}
} else {
if err := validateStructType(fncType.In(2)); err != nil {
return h, errors.Errorf("third arg %v is not valid body struct type", fncType.In(2))
if err := validateStructType(fncType.In(1)); err != nil {
return h, errors.Errorf("second arg %v is not valid body struct type", fncType.In(1))
}
}
......@@ -57,7 +49,7 @@ func NewHandler(fnc interface{}) (handler, error) {
//UserID must be int64 or *int64 with tag =???
//Username must be string with tag =???
h.RequestBodyType = fncType.In(2)
h.RequestBodyType = fncType.In(1)
}
//last result must be error
......
......@@ -53,14 +53,15 @@ func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGat
}
// service context invoke the starters and could fail, e.g. if cannot connect to db
ctx, err := api.NewContext(baseCtx, requestID, apiGatewayProxyReq)
Ctx, err = api.NewContext(baseCtx, requestID, apiGatewayProxyReq)
if err != nil {
return res, err
}
//report handler crashes
if api.crashReporter != nil {
defer api.crashReporter.Catch(ctx)
defer api.crashReporter.Catch(Ctx)
}
defer func() {
......@@ -73,9 +74,9 @@ func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGat
}()
defer func() {
ctx.LogAPIRequestAndResponse(res, err)
Ctx.LogAPIRequestAndResponse(res, err)
if err != nil {
ctx.Errorf("failed: %+v", err)
Ctx.Errorf("failed: %+v", err)
//try to retrieve HTTP code from error
if withCause, ok := err.(errors.ErrorWithCause); ok && withCause.Code() != 0 {
......@@ -96,16 +97,16 @@ func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGat
err = nil //never pass error back to lambda or http server
}
if api.requestIDHeaderKey != "" {
res.Headers[api.requestIDHeaderKey] = ctx.RequestID()
res.Headers[api.requestIDHeaderKey] = Ctx.RequestID()
}
if err := api.Service.WriteValues(ctx.StartTime(), time.Now(), ctx.RequestID(), map[string]interface{}{
if err := api.Service.WriteValues(Ctx.StartTime(), time.Now(), Ctx.RequestID(), map[string]interface{}{
"direction": "incoming",
"type": "api",
"request_id": ctx.RequestID(),
"request": ctx.Request(),
"request_id": Ctx.RequestID(),
"request": Ctx.Request(),
"response": res},
); err != nil {
ctx.Errorf("failed to audit: %+v", err)
Ctx.Errorf("failed to audit: %+v", err)
}
}()
......@@ -120,33 +121,29 @@ func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGat
for checkName, check := range api.checks {
var checkData interface{}
checkData, err = check.Check(ctx)
checkData, err = check.Check(Ctx)
if err != nil {
err = errors.Wrapf(err, "%s", checkName)
return
}
if err = ctx.Set(checkName, checkData); err != nil {
if err = Ctx.Set(checkName, checkData); err != nil {
err = errors.Wrapf(err, "failed to set check(%s) data=(%T)%+v", checkName, checkData, checkData)
return
}
}
//LEGACY: delete this as soon as all handlers accepts context
//this does not support concurrent execution!
CurrentRequestID = &apiGatewayProxyReq.RequestContext.RequestID
ctx.Debugf("HTTP %s %s ...\n", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
ctx.WithFields(map[string]interface{}{
"http_method": ctx.Request().HTTPMethod,
"path": ctx.Request().Path,
"api_gateway_request_id": ctx.Request().RequestContext.RequestID,
"user_cognito_auth_provider": ctx.Request().RequestContext.Identity.CognitoAuthenticationProvider,
"user_arn": ctx.Request().RequestContext.Identity.UserArn,
Ctx.Debugf("HTTP %s %s ...\n", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
Ctx.WithFields(map[string]interface{}{
"http_method": Ctx.Request().HTTPMethod,
"path": Ctx.Request().Path,
"api_gateway_request_id": Ctx.Request().RequestContext.RequestID,
"user_cognito_auth_provider": Ctx.Request().RequestContext.Identity.CognitoAuthenticationProvider,
"user_arn": Ctx.Request().RequestContext.Identity.UserArn,
}).Infof("Start API Handler")
//TODO:
// // Get claims and check the status of the user
// ctx.Claims, err = api.RetrieveClaims(&apiGatewayProxyReq)
// Ctx.Claims, err = api.RetrieveClaims(&apiGatewayProxyReq)
// if err != nil {
// return events.APIGatewayProxyResponse{
// StatusCode: http.StatusBadRequest,
......@@ -155,20 +152,20 @@ func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGat
// }, nil
// }
// if ctx.Claims.UserID != nil {
// userStatusResponse := checkUserStatus(ctx.Claims)
// if Ctx.Claims.UserID != nil {
// userStatusResponse := checkUserStatus(Ctx.Claims)
// if userStatusResponse != nil {
// return *userStatusResponse, nil
// }
// }
// permissionString := fmt.Sprintf("API_%s%s:%s", os.Getenv("MICRO_SERVICE_API_BASE_PATH"), apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod)
// if !permissions.HasPermission(ctx.Claims.Role, permissionString) {
// if !permissions.HasPermission(Ctx.Claims.Role, permissionString) {
// response, _ := apierr.ClientError(http.StatusUnauthorized, fmt.Sprintf("You do not have access to the requested resource: %s", permissionString))
// if ctx.Claims.Role == nil {
// ctx.Errorf("%d :: %s: %v", *ctx.Claims.RoleID, permissionString, fmt.Errorf("you have no role"))
// } else if ctx.Claims.RoleID == nil {
// ctx.Errorf("%s: you have no role ID", permissionString)
// if Ctx.Claims.Role == nil {
// Ctx.Errorf("%d :: %s: %v", *Ctx.Claims.RoleID, permissionString, fmt.Errorf("you have no role"))
// } else if Ctx.Claims.RoleID == nil {
// Ctx.Errorf("%s: you have no role ID", permissionString)
// }
// return response, nil
// }
......@@ -181,7 +178,7 @@ func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGat
}
if legacyHandlerFunc, ok := resourceHandler.(func(req events.APIGatewayProxyRequest) (response events.APIGatewayProxyResponse, err error)); ok {
ctx.Debugf("Calling legacy handler...")
Ctx.Debugf("Calling legacy handler...")
return legacyHandlerFunc(apiGatewayProxyReq)
}
......@@ -193,44 +190,43 @@ func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGat
//new type of handler function
//allocate, populate and validate params struct
paramsStruct, paramsErr := ctx.GetRequestParams(handler.RequestParamsType)
paramsStruct, paramsErr := Ctx.GetRequestParams(handler.RequestParamsType)
if paramsErr != nil {
err = errors.HTTP(http.StatusBadRequest, paramsErr, "invalid parameters")
return
}
//apply claims to params struct - TODO: Removed - see if cannot force to get claims from context always
// if err = ctx.Claims.FillOnObject(ctx.request, &paramsStruct); err != nil {
// if err = Ctx.Claims.FillOnObject(Ctx.request, &paramsStruct); err != nil {
// err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on parameters")
// return
// }
ctx.Debugf("Params: (%T) %+v", paramsStruct, paramsStruct)
Ctx.Debugf("Params: (%T) %+v", paramsStruct, paramsStruct)
args := []reflect.Value{
reflect.ValueOf(ctx),
reflect.ValueOf(paramsStruct),
}
var bodyStruct interface{}
if handler.RequestBodyType != nil {
//allocate, populate and validate request struct
bodyStruct, err = ctx.GetRequestBody(handler.RequestBodyType)
bodyStruct, err = Ctx.GetRequestBody(handler.RequestBodyType)
if err != nil {
err = errors.HTTP(http.StatusBadRequest, err, "invalid body")
return
}
//apply claims to request struct - TODO: Removed - see if cannot force to get claims from context always
// if err = ctx.Claims.FillOnObject(ctx.request, &bodyStruct); err != nil {
// if err = Ctx.Claims.FillOnObject(Ctx.request, &bodyStruct); err != nil {
// err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on body")
// return
// }
ctx.Debugf("Body: (%T) %+v", bodyStruct, bodyStruct)
Ctx.Debugf("Body: (%T) %+v", bodyStruct, bodyStruct)
args = append(args, reflect.ValueOf(bodyStruct))
}
//call handler in a func with defer to catch potential crash
ctx.Infof("Calling handle %s %s ...", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
Ctx.Infof("Calling handle %s %s ...", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
var results []reflect.Value
results, err = func() (results []reflect.Value, err error) {
defer func() {
......@@ -248,7 +244,7 @@ func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGat
return
}
//ctx.Debugf("handler -> results: %v", results)
//Ctx.Debugf("handler -> results: %v", results)
//see if handler failed using last result of type error
lastResultValue := results[len(results)-1]
if !lastResultValue.IsNil() {
......@@ -269,7 +265,7 @@ func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGat
if len(results) > 1 {
responseStruct := results[0].Interface()
ctx.Debugf("Response type: %T", responseStruct)
Ctx.Debugf("Response type: %T", responseStruct)
var bodyBytes []byte
bodyBytes, err = json.Marshal(responseStruct)
......
......@@ -12,6 +12,9 @@ import (
"gitlab.com/uafrica/go-utils/string_utils"
)
// Ctx stores lambda-wide context e.g. claims, request ID etc.
var Ctx Context
type Context interface {
context.Context
logger.Logger
......@@ -71,7 +74,7 @@ func (s service) NewContext(base context.Context, requestID string, values map[s
l := logger.New().WithFields(values)
l.IFormatter = l.IFormatter.NextColor()
ctx := &serviceContext{
Ctx = &serviceContext{
Context: base,
Logger: l,
Producer: s.Producer,
......@@ -84,18 +87,20 @@ func (s service) NewContext(base context.Context, requestID string, values map[s
for starterName, starter := range s.starters {
var starterData interface{}
starterData, err := starter.Start(ctx)
starterData, err := starter.Start(Ctx)
if err != nil {
ctx.Errorf("Start(%s) failed: %+v ...", starterName, err)
Ctx.Errorf("Start(%s) failed: %+v ...", starterName, err)
return nil, errors.Wrapf(err, "%s", starterName)
}
if err = ctx.Set(starterName, starterData); err != nil {
ctx.Errorf("Start(%s) failed to set (%T)%+v: %+v ...", starterName, starterData, starterData, err)
if err = Ctx.Set(starterName, starterData); err != nil {
Ctx.Errorf("Start(%s) failed to set (%T)%+v: %+v ...", starterName, starterData, starterData, err)
return nil, errors.Wrapf(err, "failed to set starter(%s) data=(%T)%+v", starterName, starterData, starterData)
}
ctx.Debugf("Start(%s)=(%T)%+v", starterName, starterData, starterData)
Ctx.Debugf("Start(%s)=(%T)%+v", starterName, starterData, starterData)
}
return ctx, nil
return Ctx, nil
}
type serviceContext struct {
......
......@@ -46,7 +46,7 @@ func IsNumericString(s string) bool {
return err == nil
}
// Standardise phone numbers with +27 instead of 0 prefix
// StandardisePhoneNumber standardises phone numbers with +27 instead of 0 prefix
func StandardisePhoneNumber(number string) string {
// is the first rune/char of the string a 0
if []rune(number)[0] == []rune("0")[0] {
......@@ -92,7 +92,7 @@ func UnwrapString(s *string) string {
return *s
}
//trim specified strings, replacing empty string with nil
// TrimP trims specified strings, replacing empty string with nil
func TrimP(sp *string) *string {
if sp == nil {
return nil
......@@ -104,7 +104,7 @@ func TrimP(sp *string) *string {
return &s
}
//concatenate all specified non-empty strings with ", " separators
// ConcatP concatenates all specified non-empty strings with ", " separators
func ConcatP(args ...*string) string {
s := ""
for _, arg := range args {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment