diff --git a/api/api.go b/api/api.go index 2b4e1041cd2263fcaba511384c252aa697994a27..0033526bb8f13eb31d5ca982c8eb03ae7d3fc9a4 100644 --- a/api/api.go +++ b/api/api.go @@ -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 diff --git a/api/handler.go b/api/handler.go index 3432cfb00a18e0d5ba05bf1a9d6530bd5c30dd57..f089e582b16407169e40dabae450ef73ebe573b9 100644 --- a/api/handler.go +++ b/api/handler.go @@ -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 diff --git a/api/lambda.go b/api/lambda.go index 5e8e23f84bb538f92bf8f2826db376fc094685c8..3d2479e19a8ff1b07802421cbae78317b94fdf1d 100644 --- a/api/lambda.go +++ b/api/lambda.go @@ -52,15 +52,16 @@ 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) + // service context invoke the starters and could fail, e.g. if cannot connect to db + 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, ¶msStruct); err != nil { + // if err = Ctx.Claims.FillOnObject(Ctx.request, ¶msStruct); 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) diff --git a/service/context.go b/service/context.go index 79a7522cdd8ae3153191056383ad621779ba06ca..ce4acc20d11e657db6d6a7209e271f326f7da06a 100644 --- a/service/context.go +++ b/service/context.go @@ -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 { diff --git a/string_utils/string_utils.go b/string_utils/string_utils.go index 727afecadbc28c7799ebd8d7812d7f135e823d6e..42500203ebecbd6921dad63a7919e9fab697d123 100644 --- a/string_utils/string_utils.go +++ b/string_utils/string_utils.go @@ -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 {