package api import ( "fmt" "net/http" "os" "runtime/debug" "github.com/aws/aws-lambda-go/lambda" "gitlab.com/uafrica/go-utils/audit" "gitlab.com/uafrica/go-utils/errors" queues_mem "gitlab.com/uafrica/go-utils/queues/mem" queues_sqs "gitlab.com/uafrica/go-utils/queues/sqs" "gitlab.com/uafrica/go-utils/service" "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 //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 { if requestIDHeaderKey == "" { requestIDHeaderKey = "request-id" } router, err := NewRouter(routes) if err != nil { panic(fmt.Sprintf("cannot create router: %+v", err)) } return Api{ Service: service.New(), router: router, requestIDHeaderKey: requestIDHeaderKey, checks: map[string]ICheck{}, crashReporter: defaultCrashReporter{}, cors: nil, localPort: 0, localQueueEventHandlers: nil, } } type Api struct { service.Service router Router requestIDHeaderKey string checks map[string]ICheck crashReporter ICrashReporter cors ICORS localPort int //==0 for default lambda, >0 for http.ListenAndServe to run locally localQueueEventHandlers map[string]interface{} //only applies when running locally for local in-memory queues } //wrap Service.WithStarter to return api, else cannot be chained func (api Api) WithStarter(name string, starter service.IStarter) Api { api.Service = api.Service.WithStarter(name, starter) return api } //wrap Service.WithErrorReporter to return api, else cannot be chained func (api Api) WithErrorReporter(reporter service.IErrorReporter) Api { api.Service = api.Service.WithErrorReporter(reporter) return api } //wrap else cannot be chained func (api Api) WithAuditor(auditor audit.Auditor) Api { api.Service = api.Service.WithAuditor(auditor) return api } //wrap else cannot be chained func (api Api) WithProducer(producer service.Producer) Api { api.Service = api.Service.WithProducer(producer) return api } //add a check to startup of each context //they will be called in the sequence they were added //if check return error, processing stops and err is returned //if check succeed, and return !=nil data, it is stored against the name // so your handler can retieve it with: // checkData := ctx.Value(name).(expectedType) // or // checkData,ok := ctx.Value(name).(expectedType) // if !ok { ... } //you can implement one check that does everything and return a struct or //implement one for your db, one for rate limit, one for auth, one for ... //the name must be snake-case, e.g. "this_is_my_check_data_name" func (api Api) WithCheck(name string, check ICheck) Api { if !string_utils.IsSnakeCase(name) { panic(errors.Errorf("invalid check name=\"%s\", expecting snake_case names only", name)) } if check == nil { panic(errors.Errorf("check(%s) func==nil", name)) } if _, ok := api.checks[name]; ok { panic(errors.Errorf("check(%s) already defined", name)) } api.checks[name] = check return api } func (api Api) WithCORS(cors ICORS) Api { if cors != nil { api.cors = cors } return api } func (api Api) WithCrashReported(crashReporter ICrashReporter) Api { if crashReporter != nil { api.crashReporter = crashReporter } return api } //If local port is defined (!=nil and >0) then the lambda function //is replaced with a local HTTP server func (api Api) WithLocalPort(localPortPtr *int) Api { if api.localPort != 0 { panic("local port already defined") } if localPortPtr != nil && *localPortPtr > 0 { api.localPort = *localPortPtr } return api } //WithEvents are not used in production, only when env LOG_LEVEL=debug //then the SQS producer is replaced with in-memory producer that uses //go channels to queue and process events, so they can be debugged locally func (api Api) WithEvents(eventHandlers map[string]interface{}) Api { if api.localQueueEventHandlers != nil { panic("local queue event handlers already defined") } api.localQueueEventHandlers = eventHandlers return api } //run and panic on error func (api Api) Run() { //decide local or SQS if (api.localPort > 0 || os.Getenv("LOG_LEVEL") == "debug") && api.localQueueEventHandlers != nil { //use in-memory channels for async events api.Debugf("Using in-memory channels for async events ...") memConsumer := queues_mem.NewConsumer(api.Service, api.localQueueEventHandlers) api = api.WithProducer(queues_mem.NewProducer(memConsumer)) } else { //use SQS for async events api.Debugf("Using SQS queue producer for async events ...") api = api.WithProducer(queues_sqs.NewProducer(api.requestIDHeaderKey)) } //decide local of lambda if api.localPort > 0 { //running locally with standard HTTP server err := http.ListenAndServe(fmt.Sprintf(":%d", api.localPort), api) //calls api.ServeHTTP() which calls api.Handler() if err != nil { panic(err) } } else { //run as an AWS Lambda function lambda.Start(api.Handler) } } type defaultCrashReporter struct{} func (defaultCrashReporter) Catch(ctx Context) { crashErr := recover() if crashErr != nil { ctx.Errorf("crashed: %v, with stack: %s", crashErr, string(debug.Stack())) } }