package api import ( "fmt" "net/http" "os" "regexp" "time" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "gitlab.com/uafrica/go-utils/errors" "gitlab.com/uafrica/go-utils/logger" "gitlab.com/uafrica/go-utils/service" ) //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 //requestIDHeaderKey is defined in the response header to match the requestID from the request func New(requestIDHeaderKey string, routes map[string]map[string]interface{}) Api { env := os.Getenv("ENVIRONMENT") //todo: support config loading for local dev and env for lambda in prod if env == "" { env = "dev" } if requestIDHeaderKey == "" { requestIDHeaderKey = "request-id" } router, err := NewRouter(routes) if err != nil { panic(fmt.Sprintf("cannot create router: %+v", err)) } return Api{ ILogger: logger.New().WithFields(map[string]interface{}{"env": env}), env: env, router: router, requestIDHeaderKey: requestIDHeaderKey, checks: []check{}, crashReporter: defaultCrashReporter{}, auditor: noAudit{}, } } type Api struct { logger.ILogger //for logging outside of context env string router Router requestIDHeaderKey string //options: localPort int //==0 for lambda, >0 for http.ListenAndServe crashReporter ICrashReporter cors ICORS dbConn service.IDatabaseConnector checks []check auditor IAuditor } //checks are executed at the start of each request //to stop processing, return an error, preferably with an HTTP status code (see errors.HTTP(...)) //all values returned will be logged and added to the context for retrieval by any handler //the values are added with the check's name as prefix: "<check.name>_<value.name>" //and cannot be changed afterwards type IChecker interface { Check(req events.APIGatewayProxyRequest) (values map[string]interface{}, err error) } type check struct { name string checker IChecker } const namePattern = `[a-z]([a-z0-9_]*[a-z0-9])*` var nameRegex = regexp.MustCompile("^" + namePattern + "$") //check name must be lowercase with optional underscores, e.g. "rate_limiter" or "claims" func (app Api) WithCheck(name string, checker IChecker) Api { if !nameRegex.MatchString(name) { panic(errors.Errorf("invalid name for check(%s)", name)) } if checker == nil { panic(errors.Errorf("check(%s) is nil", name)) } for _, check := range app.checks { if check.name == name { panic(errors.Errorf("check(%s) already registered", name)) } } app.checks = append(app.checks, check{name: name, checker: checker}) return app } func (api Api) WithDb(dbConn service.IDatabaseConnector) Api { api.dbConn = dbConn return api } func (api Api) WithCORS(cors ICORS) Api { api.cors = cors return api } type ICORS interface { CORS() map[string]string //return CORS headers } func (api Api) WithCrashReported(crashReporter ICrashReporter) Api { if crashReporter != nil { api.crashReporter = crashReporter } return api } type ICrashReporter interface { Catch(ctx Context) //Report(method string, path string, crash interface{}) } func (api Api) WithLocalPort(localPortPtr *int) Api { if localPortPtr != nil && *localPortPtr > 0 { api.localPort = *localPortPtr } return api } //run and panic on error func (api Api) Run() { //decide local of lambda if api.localPort > 0 { err := http.ListenAndServe(fmt.Sprintf(":%d", api.localPort), api) //calls app.ServeHTTP() which calls app.Handler() if err != nil { panic(err) } } else { lambda.Start(api.Handler) //calls app.Handler directly } } type defaultCrashReporter struct{} func (defaultCrashReporter) Catch(ctx Context) { // crash := recover() // if crash != nil { // ctx.Errorf("CRASH: (%T) %+v\n", crash, crash) // } } type IAuditor interface { Audit(startTime, endTime time.Time, values map[string]interface{}) } type noAudit struct{} func (noAudit) Audit(startTime, endTime time.Time, values map[string]interface{}) {} //do nothing