Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,4 @@ linters-settings:
disabled: true

misspell:
locale: "US" # Fix the colour => color, and co
locale: "UK" # Fix the colour => color, and co
31 changes: 14 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,34 @@ Goliath is an opinionated set of libraries to build resilient, scalable and main

# 🚀 Features

- [apperror](/apperror) - create elegant application errors
- [appcontext](/appcontext/) - wrapper around the native `ctx.Context`
- [app](/app) - set of common utilities such as elegant errors and enriched `ctx.Context`
- [retry](/retry/) - retry a specific task securely
- [clock](/clock) - wrapper around `time.Now` to help during testing
- [sleep](/sleep) - wrapper around `time.Sleep` for testing
- [httperror](/httperror) - implementation of the [RFC7807 Problem Details](https://datatracker.ietf.org/doc/html/rfc7807)

# 👀 Examples

### apperror
### app
```go
// create application errors
err := apperror.NewValidation("validate_user", "Error Validating User")
err.AddValidationError(NewValidationError("name", "name is empty"))
err.AddValidationError(NewValidationError("age", "user is under 18", "user must be an adult"))
// create elegant application errors
err := app.NewValidation("validate_user", "Error Validating User")
err.AddValidationError(NewFieldValidationError("name", "name is empty"))
err.AddValidationError(NewFieldValidationError("age", "user is under 18", "user must be an adult"))

// wrap the inner cause of the error
conn, err := db.Connect(...)

if err != nil {
return apperror.NewInternal("database_connection", "Error connecting to the database").SetSeverity(apperror.Critical).Wrap(err)
return app.NewInternalError("database_connection", "Error connecting to the database").SetSeverity(app.Critical).Wrap(err)
}

// create a raw error
err := apperror.New(("error_code", apperror.Internal, apperror.High, "Error message"))
```
err := app.NewError(("error_code", app.Internal, app.High, "Error message"))

### appcontext
```go
// enriched context
func Hello(w http.ResponseWriter, r *http.Request) {
ctx := appcontext.FromContext(r.Context())
ctx := app.FromContext(r.Context())
traceId := appCtx.TraceID()
userID, checkUser := ctx.UserID()

Expand Down Expand Up @@ -93,10 +90,10 @@ func Hello(w http.ResponseWriter, r *http.Request) {

### httperror
```go
appCtx := appcontext.FromContext(ctx)
err := apperror.NewValidation("invalid_payment_data", "The payment request is invalid")
err.AddValidationError(apperror.NewValidationError("amount", "Amount needs to be positive"))
err.AddValidationError(apperror.NewValidationError("currency", "currency is required"))
appCtx := app.FromContext(ctx)
err := app.NewValidationError("invalid_payment_data", "The payment request is invalid")
err.AddValidationError(app.NewFieldValidationError("amount", "Amount needs to be positive"))
err.AddValidationError(app.NewFieldValidationError("currency", "currency is required"))

httpErr := New(appCtx, err, "/payments")
```
Expand Down
100 changes: 100 additions & 0 deletions app/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package app

import (
"context"

"github.com/google/uuid"
)

type ContextKey string

const (
TraceIDKey ContextKey = "trace_id"
UserIDKey ContextKey = "user_id"
TenantIDKey ContextKey = "tenant_id"
)

// Context carries the context of the current execution.
type Context struct {
// original context
context.Context
}

// UserID returns the user id
func (sc *Context) UserID() (string, bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure why you return the bool about the presence, you are not doing it for TraceID

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that TraceID() always exist, so I set it in the New() if it is absent. To allow debugging easily.

userID := sc.Context.Value(UserIDKey)

if id, ok := userID.(string); ok {
return id, ok
}

return "", false
}

// SetUserID sets the user id
func (sc *Context) SetUserID(userID string) *Context {
sc.Context = context.WithValue(sc.Context, UserIDKey, userID)

return sc
}

// TenantID returns the tenant id
func (sc *Context) TenantID() (string, bool) {
tenantIDKey := sc.Context.Value(TenantIDKey)

if id, ok := tenantIDKey.(string); ok {
return id, true
}

return "", false
}

// SetTenantID sets the user id
func (sc *Context) SetTenantID(tenantID string) *Context {
sc.Context = context.WithValue(sc.Context, TenantIDKey, tenantID)

return sc
}

// SetTraceID sets the trace id
func (sc *Context) SetTraceID(traceID string) *Context {
sc.Context = context.WithValue(sc.Context, TraceIDKey, traceID)

return sc
}

// TraceID returns the trace identifier for the current flow
func (sc *Context) TraceID() string {
traceID := sc.Context.Value(TraceIDKey)

if id, ok := traceID.(string); ok {
return id
}

return ""
}

// FromContext returns a new Context from a context.Context
func FromContext(ctx context.Context) Context {
appCtx := NewContext(ctx)

if traceID, ok := ctx.Value(TraceIDKey).(string); ok {
appCtx.SetTraceID(traceID)
}

if userID, ok := ctx.Value(UserIDKey).(string); ok {
appCtx.SetUserID(userID)
}

if tenantID, ok := ctx.Value(TenantIDKey).(string); ok {
appCtx.SetTenantID(tenantID)
}

return appCtx
}

// NewContext returns a new Context
func NewContext(ctx context.Context) Context {
ctx = context.WithValue(ctx, TraceIDKey, uuid.NewString())
return Context{Context: ctx}
}
38 changes: 19 additions & 19 deletions appcontext/app_context_test.go → app/context_test.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
package appcontext
package app

import (
"context"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewAppContext(t *testing.T) {
func TestNewContext(t *testing.T) {
parentCtx := context.Background()

ctx := NewAppContext(parentCtx)
ctx := NewContext(parentCtx)

assert.NotEmpty(t, ctx.TraceID())
require.NotEmpty(t, ctx.TraceID())

_, err := uuid.Parse(ctx.TraceID())

assert.NoError(t, err)
require.NoError(t, err)
}

func TestFromContext(t *testing.T) {
Expand All @@ -31,38 +31,38 @@ func TestFromContext(t *testing.T) {

ctx := FromContext(parentCtx)

assert.Equal(t, expectedTraceID, ctx.TraceID())
require.Equal(t, expectedTraceID, ctx.TraceID())

userID, checkUser := ctx.UserID()
assert.Equal(t, expectedUserID, userID)
assert.True(t, checkUser)
require.Equal(t, expectedUserID, userID)
require.True(t, checkUser)

tenantID, checkTenant := ctx.TenantID()
assert.Equal(t, expectedTenantID, tenantID)
assert.True(t, checkTenant)
require.Equal(t, expectedTenantID, tenantID)
require.True(t, checkTenant)
}

func TestFromEmptyContext(t *testing.T) {
parentCtx := context.Background()
ctx := FromContext(parentCtx)

assert.NotEmpty(t, ctx.TraceID())
require.NotEmpty(t, ctx.TraceID())

userID, checkUser := ctx.UserID()
assert.Equal(t, "", userID)
assert.False(t, checkUser)
require.Equal(t, "", userID)
require.False(t, checkUser)

tenantID, checkTenant := ctx.TenantID()
assert.Equal(t, "", tenantID)
assert.False(t, checkTenant)
require.Equal(t, "", tenantID)
require.False(t, checkTenant)
}

func TestAppContextCancellation(t *testing.T) {
func TestContextCancellation(t *testing.T) {
parentCtx, cancel := context.WithCancel(context.Background())

ctx := NewAppContext(parentCtx)
ctx := NewContext(parentCtx)

cancel()

assert.Equal(t, context.Canceled, ctx.Context().Err())
require.Equal(t, context.Canceled, ctx.Err())
}
Loading