Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
29 changes: 29 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,35 @@ func addHelpLink(e *TrogonError, description, url string) {
})
}

type trogonError interface {
Is(error) bool
}

// As checks if the error matches the target and returns the TrogonError if it does.
// This combines error matching and error extraction in a single, more idiomatic operation.
// The target can be either a TrogonError or an ErrorTemplate.
// Returns the TrogonError and true if the error matches, nil and false otherwise.
//
// Example usage:
//
// if trogonErr, ok := trogonerror.As(err, users.ErrUserNotFound); ok {
// return trogonErr.WithChanges(
// trogonerror.WithChangeMetadataValue(trogonerror.VisibilityPublic, "user_id", req.UserID),
// )
// }
func As(err error, target trogonError) (*TrogonError, bool) {
var trogonErr *TrogonError
if !errors.As(err, &trogonErr) {
return nil, false
}

if !target.Is(trogonErr) {
return nil, false
}

return trogonErr, true
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func addMetadataValue(e *TrogonError, visibility Visibility, key, value string) {
if len(e.metadata) == 0 {
e.metadata = make(Metadata)
Expand Down
132 changes: 132 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,138 @@ func (c CustomError) Error() string {
return c.msg
}

func TestAs(t *testing.T) {
t.Run("As returns TrogonError when error matches", func(t *testing.T) {
template := trogonerror.NewErrorTemplate("shopify.inventory", "INSUFFICIENT_INVENTORY",
trogonerror.TemplateWithCode(trogonerror.CodeFailedPrecondition))

originalErr := template.NewError(
trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "productId", "gid://shopify/Product/1234567890"))

trogonErr, ok := trogonerror.As(originalErr, template)
assert.True(t, ok)
assert.NotNil(t, trogonErr)
assert.Equal(t, "shopify.inventory", trogonErr.Domain())
assert.Equal(t, "INSUFFICIENT_INVENTORY", trogonErr.Reason())
assert.Equal(t, "gid://shopify/Product/1234567890", trogonErr.Metadata()["productId"].Value())

})

t.Run("As returns false when error doesn't match", func(t *testing.T) {
template1 := trogonerror.NewErrorTemplate("shopify.inventory", "INSUFFICIENT_INVENTORY")
template2 := trogonerror.NewErrorTemplate("shopify.users", "NOT_FOUND")

err1 := template1.NewError()

trogonErr, ok := trogonerror.As(err1, template2)
assert.False(t, ok)
assert.Nil(t, trogonErr)
})

t.Run("As returns false for non-TrogonError", func(t *testing.T) {
template := trogonerror.NewErrorTemplate("shopify.inventory", "INSUFFICIENT_INVENTORY")
regularErr := errors.New("regular error")

trogonErr, ok := trogonerror.As(regularErr, template)
assert.False(t, ok)
assert.Nil(t, trogonErr)
})

t.Run("As works with WithChanges pattern", func(t *testing.T) {
template := trogonerror.NewErrorTemplate("shopify.inventory", "INSUFFICIENT_INVENTORY",
trogonerror.TemplateWithCode(trogonerror.CodeResourceExhausted))

originalErr := template.NewError(
trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "productId", "gid://shopify/Product/1234567890"))

trogonErr, ok := trogonerror.As(originalErr, template)
assert.True(t, ok)
assert.NotNil(t, trogonErr)

modifiedErr := trogonErr.WithChanges(
trogonerror.WithChangeMetadataValue(trogonerror.VisibilityPublic, "main_order_id", "order_123"),
trogonerror.WithChangeMetadataValue(trogonerror.VisibilityPublic, "listing_count", "5"),
)

assert.Equal(t, "order_123", modifiedErr.Metadata()["main_order_id"].Value())
assert.Equal(t, "5", modifiedErr.Metadata()["listing_count"].Value())
assert.Equal(t, "gid://shopify/Product/1234567890", modifiedErr.Metadata()["productId"].Value()) // Original preserved
})

t.Run("As works with TrogonError directly", func(t *testing.T) {
originalErr := trogonerror.NewError("shopify.inventory", "INSUFFICIENT_INVENTORY",
trogonerror.WithCode(trogonerror.CodeFailedPrecondition),
trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "productId", "gid://shopify/Product/1234567890"))

// Test with TrogonError as target (not template)
trogonErr, ok := trogonerror.As(originalErr, originalErr)
assert.True(t, ok)
assert.NotNil(t, trogonErr)
assert.Equal(t, "shopify.inventory", trogonErr.Domain())
assert.Equal(t, "INSUFFICIENT_INVENTORY", trogonErr.Reason())
})

t.Run("As works with wrapped TrogonError", func(t *testing.T) {
template := trogonerror.NewErrorTemplate("shopify.inventory", "INSUFFICIENT_INVENTORY")

originalErr := template.NewError(
trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "productId", "gid://shopify/Product/1234567890"))

// Wrap the TrogonError with fmt.Errorf (this was the problematic scenario)
wrappedErr := fmt.Errorf("context: %w", originalErr)

// Test the As function with the wrapped error
trogonErr, ok := trogonerror.As(wrappedErr, template)
assert.True(t, ok, "As should return true for wrapped TrogonError")
assert.NotNil(t, trogonErr, "As should return non-nil TrogonError")

// Verify the extracted error has the correct properties
assert.Equal(t, "shopify.inventory", trogonErr.Domain())
assert.Equal(t, "INSUFFICIENT_INVENTORY", trogonErr.Reason())
assert.Equal(t, "gid://shopify/Product/1234567890", trogonErr.Metadata()["productId"].Value())
})

}

func TestInternalMethods(t *testing.T) {
t.Run("TrogonError.is method delegates to Is", func(t *testing.T) {
err1 := trogonerror.NewError("shopify.session", "SESSION_EXPIRED")
err2 := trogonerror.NewError("shopify.session", "SESSION_EXPIRED")
err3 := trogonerror.NewError("shopify.session", "SESSION_INVALID")

// Test the internal is method by using it in the As function
// This indirectly tests the is method since As calls target.is(err)
trogonErr, ok := trogonerror.As(err2, err1)
assert.True(t, ok)
assert.NotNil(t, trogonErr)

trogonErr2, ok2 := trogonerror.As(err3, err1)
assert.False(t, ok2)
assert.Nil(t, trogonErr2)
})

t.Run("ErrorTemplate.is method delegates to Is", func(t *testing.T) {
template := trogonerror.NewErrorTemplate("shopify.session", "SESSION_EXPIRED")
err1 := template.NewError()
err2 := trogonerror.NewError("shopify.session", "SESSION_EXPIRED")
err3 := trogonerror.NewError("shopify.session", "SESSION_INVALID")

// Test the internal is method by using it in the As function
// This indirectly tests the is method since As calls target.is(err)
trogonErr, ok := trogonerror.As(err1, template)
assert.True(t, ok)
assert.NotNil(t, trogonErr)

trogonErr2, ok2 := trogonerror.As(err2, template)
assert.True(t, ok2)
assert.NotNil(t, trogonErr2)

trogonErr3, ok3 := trogonerror.As(err3, template)
assert.False(t, ok3)
assert.Nil(t, trogonErr3)
})
}

func TestErrorTemplate(t *testing.T) {
t.Run("NewErrorTemplate creates template with defaults", func(t *testing.T) {
template := trogonerror.NewErrorTemplate("shopify.session", "SESSION_EXPIRED")
Expand Down
34 changes: 34 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,37 @@ func ExampleWithMetadataValuef_formattedValues() {
// Amount: $299.99
// Request ID: req_1234567890_5432109876
}

func ExampleAs_idiomaticErrorHandling() {
// Define error templates (typically done at package level)
var ErrInsufficientStock = trogonerror.NewErrorTemplate("inventory", "INSUFFICIENT_STOCK",
trogonerror.TemplateWithCode(trogonerror.CodeResourceExhausted))

// Simulate an error from inventory service
inventoryErr := ErrInsufficientStock.NewError(
trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "product_id", "prod_12345"))

// Old verbose pattern:
// if inventory.ErrInsufficientStock.Is(err) {
// var trogonErr *trogonerror.TrogonError
// if errors.As(err, &trogonErr) {
// return nil, trogonErr.WithChanges(...)
// }
// }

// New idiomatic pattern using As:
if trogonErr, ok := trogonerror.As(inventoryErr, ErrInsufficientStock); ok {
modifiedErr := trogonErr.WithChanges(
trogonerror.WithChangeMetadataValue(trogonerror.VisibilityPublic, "order_id", "order_789"),
trogonerror.WithChangeMetadataValue(trogonerror.VisibilityPublic, "requested_quantity", "10"),
)
fmt.Printf("Modified error domain: %s\n", modifiedErr.Domain())
fmt.Printf("Order ID: %s\n", modifiedErr.Metadata()["order_id"].Value())
fmt.Printf("Requested quantity: %s\n", modifiedErr.Metadata()["requested_quantity"].Value())
}

// Output:
// Modified error domain: inventory
// Order ID: order_789
// Requested quantity: 10
}