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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.

The format loosely follows Keep a Changelog, but simplified. This project is pre-1.0; minor version bumps (0.x.y) may include breaking changes.

## [0.6.0] - 2026-05-29
### Changed (BREAKING)
- Removed the deprecated in-repo key compatibility layer.
- Removed `NewKey`, `KeyFactory`, `WithSegments`, `Key`, `KeySegment`, and `KeyOption` from `errorc`.
- Use [`github.com/ygrebnov/keys`](https://github.com/ygrebnov/keys) directly for structured keys.
- Existing field helpers such as `String`, `Int`, `Bool`, and `Error` continue to accept `keys.Key` values because they are string-based.

### Migration
- Before:
- `errorc.NewKey("id", errorc.WithSegments("user"))`
- `errorc.KeyFactory(errorc.WithSegments("user"))`
- After:
- `keys.New("id", keys.WithSegments("user"))`
- `keys.Factory(keys.WithSegments("user"))`

## [0.5.0] - 2025-11-23
### Added
- `Namespace` type shared between keys and errors for constructing namespaced identifiers.
Expand Down Expand Up @@ -53,5 +68,6 @@ Notes:
---

[0.5.0]: https://github.com/ygrebnov/errorc/releases/tag/v0.5.0
[0.6.0]: https://github.com/ygrebnov/errorc/releases/tag/v0.6.0
[0.4.0]: https://github.com/ygrebnov/errorc/releases/tag/v0.4.0
[0.2.0]: https://github.com/ygrebnov/errorc/releases/tag/v0.2.0
72 changes: 30 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ err := errorc.With(errorc.New("oops"), errorc.String("detail", "something"))
fmt.Println(err)
```

`String`, `Int`, `Bool`, and `Error` also work with [`github.com/ygrebnov/keys.Key`](https://github.com/ygrebnov/keys), since it has an underlying string type:

```go
userIDKey := keys.New("id", keys.WithSegments("user"))
err := errorc.With(errorc.New("invalid input"), errorc.String(userIDKey, "123"))
fmt.Println(err) // invalid input, user.id: 123
```

### Error (embedding an underlying cause's message)
Use `Error` to capture another error's message as a structured field. Nil errors are ignored.

Expand All @@ -98,56 +106,36 @@ err3 := errorc.With(errorc.New("operation failed"), errorc.Error("cause", nil))
fmt.Println(err3) // operation failed
```

### Structured keys with NewKey
For more structured, reusable keys you can use `NewKey` with segments. These helpers
are kept for compatibility; for new code, prefer `github.com/ygrebnov/keys` and use
`keys.New` / `keys.Factory` directly:

```go
// user.id
userKey := errorc.NewKey(
"id",
errorc.WithSegments("user"),
)

err := errorc.With(errorc.New("invalid input"), errorc.String(userKey, "123"))
fmt.Println(err) // invalid input, user.id: 123
```

Migration snippet:

```go
// Before (errorc)
userKey := errorc.NewKey("id", errorc.WithSegments("user"))

// After (keys)
userKey := keys.New("id", '.', keys.WithSegments("user"))
```

Empty segments are skipped by `WithSegments`, so they won't introduce redundant separators.

### KeyFactory (pre-bound segments)
When many keys share the same segments, `KeyFactory` helps avoid repeating
`WithSegments` calls by returning a constructor bound to those segments. These helpers
are kept for compatibility; for new code, prefer `github.com/ygrebnov/keys`.
### Structured keys
Structured keys are provided by [`github.com/ygrebnov/keys`](https://github.com/ygrebnov/keys).
Use `keys.New` or `keys.Factory` there, then pass the resulting key to `errorc.String`,
`errorc.Int`, `errorc.Bool`, or `errorc.Error`.

```go
// Create a factory for the "user" segments.
userKey := errorc.KeyFactory(errorc.WithSegments("user"))
// Direct construction.
userIDKey := keys.New("id", keys.WithSegments("user"))

// Build structured keys within these segments.
idKey := userKey("id")
// Pre-bound constructor for shared segments.
userKey := keys.Factory(keys.WithSegments("user"))
emailKey := userKey("email")

err := errorc.With(
errorc.New("invalid input"),
errorc.String(idKey, "123"),
errorc.String(userIDKey, "123"),
errorc.String(emailKey, "user@example.com"),
)
fmt.Println(err) // invalid input, user.id: 123, user.email: user@example.com
```

Empty segments passed to the factory are skipped, consistent with `WithSegments`.
Migration snippet:

```go
// Before (old errorc key helpers)
// userKey := errorc.NewKey("id", errorc.WithSegments("user"))

// After (keys)
userKey := keys.New("id", keys.WithSegments("user"))
```

### Int and Bool
Helpers for common primitive types. These convert the value once when the field is created (no repeated formatting) and follow the same formatting rules (empty key prints only the value):
Expand Down Expand Up @@ -193,11 +181,11 @@ err3 := storageErr("read_failed")
fmt.Println(err3) // storage: read_failed
```

If the message is empty, both `Namespace.NewError("")` and `ErrorFactory(...)("")`
produce an error whose `Error()` is `""` (same as `New("")`).
If the message is empty and the namespace is non-empty, both `Namespace.NewError("")`
and `ErrorFactory(...)("")` produce an error string that contains only the
namespace prefix, for example `"storage: "`.

These use the same `Namespace`/`WithNamespace` options for errors. Keys use
`KeyOption` with `WithSegments` to form identifiers like `segment1.segment2.name`.
For structured keys such as `segment1.segment2.name`, use [`github.com/ygrebnov/keys`](https://github.com/ygrebnov/keys).

## Installation

Expand Down
26 changes: 12 additions & 14 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,18 @@
// err := With(New("query failed"), Int("retries", 3), Bool("cached", false))
// // query failed, retries: 3, cached: false
//
// Keys can be composed using [NewKey] with optional [WithSegments] options.
// Segments form a prefix, followed by the base name. Empty segments are skipped. For example:
// Structured keys are provided by the github.com/ygrebnov/keys package. The generic field
// helpers in this package accept those keys directly because they have an
// underlying string type. For example:
//
// // database.user.id
// databaseUserIDKey := NewKey("id", WithSegments("database", "user"))
// err := With(New("invalid input"), String(databaseUserIDKey, "123"))
// // invalid input, database.user.id: 123
// userIDKey := keys.New("id", keys.WithSegments("user"))
// err := With(New("invalid input"), String(userIDKey, "123"))
// // invalid input, user.id: 123
//
// Key helpers in this package are maintained for compatibility. For new code,
// prefer github.com/ygrebnov/keys and use keys.New / keys.Factory directly.
// When many keys share the same segments, keys.Factory can be used to pre-bind
// those segments and create a constructor for structured keys:
//
// When many keys share the same segments, [KeyFactory] can be used to
// pre-bind those segments and create a constructor for structured keys:
//
// userKeyFactory := KeyFactory(WithSegments("user"))
// userKeyFactory := keys.Factory(keys.WithSegments("user"))
// userIDKey := userKeyFactory("id")
// userEmailKey := userKeyFactory("email")
// err := With(New("invalid input"), String(userIDKey, "123"), String(userEmailKey, "user@example.com"))
Expand All @@ -93,8 +90,9 @@
// err := storage.NewError("read_failed")
// // err.Error() == "storage: read_failed"
//
// If the message is empty, both Namespace.NewError("") and ErrorFactory(...)("")
// produce an error whose Error() is "" (same as New("")).
// If the message is empty and the namespace is non-empty, both
// Namespace.NewError("") and ErrorFactory(...)("") produce an error string
// that contains only the namespace prefix, for example "storage: ".
//
// or:
//
Expand Down
22 changes: 12 additions & 10 deletions errorc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,28 @@ import (
"unsafe"
)

// Namespace is a logical namespace for identifiers used by this package.
// It is used when constructing both namespaced error messages (via New and ErrorFactory) and
// keys (via NewKey/KeyFactory).
// Namespace is a logical namespace for error identifiers used by this package.
// It is used when constructing namespaced error messages via New, Namespace.NewError,
// and ErrorFactory.
type Namespace string

// NewError creates a new error with the given message under this namespace.
// If message is empty and the namespace is non-empty, the resulting error string
// contains only the namespace prefix, for example "storage: ".
func (n Namespace) NewError(message string) error {
return New(message, WithNamespace(n))
}

// Option defines a function that modifies the byte representation of an identifier.
// It is used when constructing namespaced errors (New/ErrorFactory) and is not
// used for key construction (see KeyOption).
// It is used when constructing namespaced errors (New, Namespace.NewError,
// and ErrorFactory).
type Option func([]byte) []byte

// WithNamespace sets a namespace prefix for an identifier. Namespace and identifier are separated by a colon.
func WithNamespace(ns Namespace) Option {
return func(b []byte) []byte {
// We store namespace bytes at the front; actual dot separators are
// inserted when composing the final message in New or final key in NewKey.
// Store the namespace bytes at the front; WithNamespace is responsible for
// inserting the ": " separator between namespace and message.
if len(ns) == 0 {
return b
}
Expand Down Expand Up @@ -64,9 +66,9 @@ func New(message string, opts ...Option) error {
}

// ErrorFactory returns a function that creates errors under the given namespace.
// It uses the same Namespace/WithNamespace options as key construction and
// produces identifiers like "ns: message". If message is empty, the returned
// error's Error() is "" (same as New("")).
// It produces identifiers like "ns: message". If message is empty and the
// namespace is non-empty, the returned error string contains only the namespace
// prefix, for example "storage: ".
func ErrorFactory(ns Namespace) func(message string) error {
return func(message string) error {
return New(message, WithNamespace(ns))
Expand Down
46 changes: 6 additions & 40 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ package errorc
import (
"errors"
"fmt"

"github.com/ygrebnov/keys"
)

func ExampleWith_sentinelError() {
// Create a new sentinel error.
ErrInvalidInput := New("invalid input")

// It is useful to keep context fields keys centralized.
Field1 := keys.New("field1")

// Wrap the sentinel error with additional context.
err := With(
ErrInvalidInput,
String("field1", "value1"),
String(Field1, "value1"),
String("field2", "value2"),
)

Expand Down Expand Up @@ -51,17 +56,6 @@ func ExampleWith_typedError() {
// Output: Handled ValidationError: invalid input, field1: value1, field2: value2
}

func ExampleString_typedKey() {
// Demonstrate using a custom named string type as a key.
type Key string
const UserID Key = "user_id"

err := With(New("invalid input"), String(UserID, "123"))
fmt.Println(err)

// Output: invalid input, user_id: 123
}

// ExampleError demonstrates adding an underlying error message as a field.
func ExampleError() {
base := New("operation failed")
Expand All @@ -87,34 +81,6 @@ func ExampleBool() {
// Output: query failed, cached: false
}

func ExampleNewKey() {
// Compose a structured key using segments, with the
// base name coming last.
userKey := NewKey("id", WithSegments("database", "user"))

err := With(New("invalid input"), String(userKey, "123"))
fmt.Println(err)

// Output: invalid input, database.user.id: 123
}

func ExampleKeyFactory() {
// Create a user key factory prepending "user" segment (prefix) to keys.
userKeyFactory := KeyFactory(WithSegments("user"))

// Build structured keys using segments.
userIDKey := userKeyFactory("id")
userEmailKey := userKeyFactory("email")

err := With(New("invalid input"),
String(userIDKey, "123"),
String(userEmailKey, "user@example.com"),
)

fmt.Println(err)
// Output: invalid input, user.id: 123, user.email: user@example.com
}

func ExampleNew_withNamespace() {
// Build a namespaced error using New and WithNamespace.
err := New("read_failed", WithNamespace("storage"))
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/ygrebnov/errorc

go 1.22

require github.com/ygrebnov/keys v0.1.0
require github.com/ygrebnov/keys v0.2.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
github.com/ygrebnov/keys v0.1.0 h1:X5JVbAZMPkbc8PWuvbX8OQbO6ZvA26ElxkbB1b3hZbg=
github.com/ygrebnov/keys v0.1.0/go.mod h1:4IfRPgv7tFSlToKzHtA9MPMwXP0+HRJ0Kk8XSrvhDvQ=
github.com/ygrebnov/keys v0.2.0 h1:KQSQG1la9WkTW59WbB3CK1yhxZ92el2ZJfV1XtlHTp0=
github.com/ygrebnov/keys v0.2.0/go.mod h1:4IfRPgv7tFSlToKzHtA9MPMwXP0+HRJ0Kk8XSrvhDvQ=
78 changes: 0 additions & 78 deletions key.go

This file was deleted.

Loading