Context
In the context of an HTTP request, "context" refers to the additional information, environment, or state that surrounds or supports the request during its lifecycle. It typically includes metadata and parameters that help process the request and manage its execution.
In summary, the "context" in an HTTP request is any auxiliary information or state passed along with the request to help with its handling, processing, or management across different layers of an application or system.
Torpedo provides an Execution Context Map that can be integrated easily with the Gin Gonic Context object and shared with entity service or even use cases.
This refers to the broader environment in which an HTTP request is being handled. It includes data such as the current user, security roles, server settings, or anything else needed to handle the request within an application.
For instance, in cloud or microservices architectures, an execution context could include tracing information for distributed systems.
Adding Execution Context Map to Gin
Torpedo lib provides a "Gin utils" package to do this integration straight forward. The context map is added into the request life cycle
as middleware, for instance:
| package dependency
import (
"github.com/darksubmarine/torpedo-lib-go/app"
"github.com/darksubmarine/torpedo-lib-go/conf"
"github.com/darksubmarine/torpedo-lib-go/http/gin_utils" //(1)!
"github.com/darksubmarine/torpedo-lib-go/log"
"github.com/gin-gonic/gin"
"fmt"
"net/http"
)
// HttpServerProvider provides a gin http server
type HttpServerProvider struct {
app.BaseProvider
cfg conf.Map
// server gin instance to be provided
server *gin.Engine `torpedo.di:"provide"`
// api router group to add endpoints under /api prefix
apiV1 *gin.RouterGroup `torpedo.di:"provide,name=APIv1"`
// binds
logger log.ILogger `torpedo.di:"bind"`
}
func NewHttpServerProvider(config conf.Map) *HttpServerProvider {
return &HttpServerProvider{cfg: config}
}
// Provide set the provide instances
func (p *HttpServerProvider) Provide(c app.IContainer) error {
// gin server default instance
p.server = gin.Default()
// Adding a /ping endpoint
p.server.GET("/ping", func(c *gin.Context) {
p.logger.Info("/ping has been called")
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
// api v1 group
p.apiV1 = p.server.Group("/api/v1")
p.apiV1.Use(gin_utils.WithDataContext()) //(2)!
return nil
}
// OnStart launches the gin http server calling the gin.Run method
func (p *HttpServerProvider) OnStart() func() error {
return func() error {
go func() {
port := p.cfg.FetchIntP("port")
if err := p.server.Run(fmt.Sprintf(":%d", port)); err != nil {
panic(fmt.Sprintf("error starting HTTP server at %d with error %s", port, err))
}
}()
return nil
}
}
|
- Import the
gin_utils
package to do the integration.
- Adding the Execution Data Context on each API v1 request.
Writing data within Execution Context Map
Once that the context map is set into each request lifecycle it is available to set key-value
data pairs. For instance,
following the sensor
controller:
| func (h *InputGin) MyAwesomeENdpoint(c *gin.Context) {
id := c.Param("id")
token := c.Request.Header["Token"]
gin_utils.SetDataContext(c, "token", token) //(1)!
ctx, _ := gin_utils.GetDataContext(c) //(2)!
if err := h.srv.Delete(ctx, id); err != nil { //(3)!
if errors.Is(err, torpedo_lib.ErrIdNotFound) {
c.JSON(http.StatusNotFound, api.ErrorNotFound(err))
} else {
c.JSON(http.StatusInternalServerError, api.ErrorEntityRemove(err))
}
return
}
c.JSON(http.StatusNoContent, nil)
}
|
- Setting the token and binding it withing Gin Context, maybe if other middlewares need it.
Method definition: SetDataContext(c *gin.Context, key string, val interface{})
- Fetching the Execution Data Context from Gin Context to shared with the service method.
- Calling service method and passing the Execution Data Context as parameter. In this example, from the service could be possible read the user token.
Reading data from Execution Context Map
The execution context map is a sync.Map
struct ready to write and read. The map implements an interface to cast key-value data:
| // IDataMap interface to defines DataMap methods.
type IDataMap interface {
Set(key string, val interface{})
Get(key string) interface{}
GetOrElse(key string, val interface{}) interface{}
GetStringOrElse(key string, val string) string
GetInt64OrElse(key string, val int64) int64
GetIntOrElse(key string, val int) int
GetFloat64OrElse(key string, val float64) float64
GetBoolOrElse(key string, val bool) bool
}
|
So, to fetch data only needs to know the key which is a string key. Also, the map provides methods to return default values in case that the given key doesn't exist.
Following the previous example we can fetch the token like:
| // Delete removes the entity given its id
func (s *ServiceBase) Delete(ctx context.IDataMap, id string) error {
hook := s.hookDelete()
hookErr := s.execHookById(hook.beforeDelete, ctx, id) //(1)!
if hookErr != nil {
s.logger.Error("before delete hook", "error", hookErr)
}
token := ctx.GetStringOrElse("token", "unknown") //(2)!
s.logger.Debug("deleting from repo", "id", id, "user-token", token)
err := s.repo.DeleteByID(id)
if err != nil {
s.logger.Error("deleting from repo", "error", err, "id", id)
return err // prevents call the after delete hook due to an error
}
hookErr = s.execHookById(hook.afterDelete, ctx, id) //(3)!
if hookErr != nil {
s.logger.Error("before delete hook", "error", hookErr)
}
return err
}
|
- At basic CRUD operations the context is passed throw the hook functions.
- Reading the token value or
unknown
value if it is not set. This is only illustrative, not included at basic CRUD operations.
- At basic CRUD operations the context is passed throw the hook functions.