diff --git a/README.md b/README.md index 87965ed..d074502 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,31 @@ if zerrErr, ok := err.(*zerr.Error); ok { } ``` +### Accessing Metadata + +You can access the structured metadata attached to errors using the `Metadata()` method: + +```go +// Create an error with metadata +err := zerr.New("database error") +err = zerr.With(err, "table", "users") +err = zerr.With(err, "operation", "insert") +err = zerr.With(err, "user_id", 12345) + +// Access metadata (requires type assertion to *zerr.Error) +if zerrErr, ok := err.(*zerr.Error); ok { + metadata := zerrErr.Metadata() + // metadata is map[string]any{"table": "users", "operation": "insert", "user_id": 12345} + + // Access individual values + if table, exists := metadata["table"]; exists { + fmt.Printf("Table: %v\n", table) + } +} +``` + +The `Metadata()` method returns a copy of the error's metadata as a `map[string]any`. It returns an empty map if there is no metadata attached to the error. The returned map is a copy, ensuring that modifications to it do not affect the internal state of the error. + ### Stack Traces Capture stack traces easily using the global `Stack` helper. @@ -136,4 +161,4 @@ paths. ## License -MIT +MIT \ No newline at end of file diff --git a/zerr.go b/zerr.go index 750599c..f4fe473 100644 --- a/zerr.go +++ b/zerr.go @@ -133,6 +133,20 @@ func (e *Error) WithStack() *Error { } } +// Metadata returns a copy of the error's metadata as a map. +// Returns an empty map if there is no metadata attached to the error. +func (e *Error) Metadata() map[string]any { + if len(e.metadata) == 0 { + return make(map[string]any) + } + + result := make(map[string]any, len(e.metadata)) + for _, meta := range e.metadata { + result[meta.key.Value()] = meta.value + } + return result +} + // Error implements the error interface. func (e *Error) Error() string { if e.cause == nil { diff --git a/zerr_test.go b/zerr_test.go index e0e7ee6..0f6f113 100644 --- a/zerr_test.go +++ b/zerr_test.go @@ -75,7 +75,7 @@ func TestWithMultipleMetadata(t *testing.T) { t.Errorf("Expected 3 metadata items, got %d", len(withErr.metadata)) } - expected := map[string]interface{}{ + expected := map[string]any{ "key1": "value1", "key2": 42, "key3": true, @@ -760,3 +760,75 @@ func ExampleWith() { fmt.Println(err.Error()) // Output: database error } + +func TestMetadata(t *testing.T) { + // Test with no metadata + testErr := New("test") + err, ok := testErr.(*Error) + if !ok { + t.Fatalf("Expected *Error type, got %T", testErr) + } + + metadata := err.Metadata() + if metadata == nil { + t.Error("Metadata should not return nil") + } + if len(metadata) != 0 { + t.Errorf("Expected empty metadata, got %d items", len(metadata)) + } + + // Test with single metadata + withErr := err.With("key", "value") + metadata = withErr.Metadata() + if len(metadata) != 1 { + t.Errorf("Expected 1 metadata item, got %d", len(metadata)) + } + if metadata["key"] != "value" { + t.Errorf("Expected metadata[key] = 'value', got %v", metadata["key"]) + } + + // Test with multiple metadata + multiErr := err.With("key1", "value1").With("key2", 42).With("key3", true) + metadata = multiErr.Metadata() + if len(metadata) != 3 { + t.Errorf("Expected 3 metadata items, got %d", len(metadata)) + } + + expected := map[string]any{ + "key1": "value1", + "key2": 42, + "key3": true, + } + + for key, expectedValue := range expected { + if value, exists := metadata[key]; !exists { + t.Errorf("Missing metadata key: %s", key) + } else if value != expectedValue { + t.Errorf("For key %s, expected %v, got %v", key, expectedValue, value) + } + } +} + +func TestMetadataImmutability(t *testing.T) { + testErr := New("test") + err, ok := testErr.(*Error) + if !ok { + t.Fatalf("Expected *Error type, got %T", testErr) + } + + withErr := err.With("key", "value") + metadata := withErr.Metadata() + + // Modify the returned map + metadata["new_key"] = "new_value" + metadata["key"] = "modified_value" + + // Get metadata again - should be unchanged + metadata2 := withErr.Metadata() + if metadata2["key"] != "value" { + t.Error("Metadata should be immutable - modifying returned map should not affect original") + } + if _, exists := metadata2["new_key"]; exists { + t.Error("Metadata should be immutable - adding keys to returned map should not affect original") + } +}