From faeced86b18ec60639cbb99b99eb6cb808335d82 Mon Sep 17 00:00:00 2001 From: Yaroslav Grebnov Date: Fri, 29 May 2026 19:46:41 +0200 Subject: [PATCH] Update dependencies --- CHANGELOG.md | 16 ++++ README.md | 72 ++++++++--------- doc.go | 26 +++---- errorc.go | 22 +++--- example_test.go | 46 ++--------- go.mod | 2 +- go.sum | 2 + key.go | 78 ------------------- key_test.go | 200 ------------------------------------------------ 9 files changed, 79 insertions(+), 385 deletions(-) delete mode 100644 key.go delete mode 100644 key_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d1c2b3..93a672c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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 diff --git a/README.md b/README.md index 3dfdf6c..c7c7ea1 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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): @@ -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 diff --git a/doc.go b/doc.go index 587de92..1a4e439 100644 --- a/doc.go +++ b/doc.go @@ -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")) @@ -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: // diff --git a/errorc.go b/errorc.go index b261055..ce6c38f 100644 --- a/errorc.go +++ b/errorc.go @@ -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 } @@ -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)) diff --git a/example_test.go b/example_test.go index 416c130..50cdf73 100644 --- a/example_test.go +++ b/example_test.go @@ -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"), ) @@ -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") @@ -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")) diff --git a/go.mod b/go.mod index 3435005..f7b0bce 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 5795b2a..3414def 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/key.go b/key.go deleted file mode 100644 index 83beb01..0000000 --- a/key.go +++ /dev/null @@ -1,78 +0,0 @@ -package errorc - -import keys "github.com/ygrebnov/keys" - -// Key is a type alias for string used as a key in error context fields. -// Deprecated: use github.com/ygrebnov/keys.Key directly. -type Key = keys.Key - -// KeySegment is a type alias for string used to define segments in keys. -// Deprecated: use github.com/ygrebnov/keys.Segment directly. -type KeySegment = keys.Segment - -// KeyOption defines a function that modifies the byte representation of an identifier. -// It is used when constructing keys (NewKey/KeyFactory) and is not used for errors -// (see Option). -// Deprecated: use github.com/ygrebnov/keys.Option directly. -type KeyOption func([]byte) []byte - -// WithSegments appends segments that will appear before name, -// each separated by a dot. Empty segments are skipped. -// Deprecated: use github.com/ygrebnov/keys.WithSegments directly. -func WithSegments(segments ...KeySegment) KeyOption { - opt := keys.WithSegments(segments...) - return func(b []byte) []byte { - return opt('.', b) - } -} - -// NewKey constructs a Key from optional segments, and the base name. -// The expected final form is: -// -// [segment1[.segment2[...]]].name -// -// where segments are provided via options and `name` is the -// base argument. For example: -// -// NewKey("user", WithSegments("org", "id")) -// -// produces: -// -// org.id.user -// -// Deprecated: use github.com/ygrebnov/keys.New with separator '.' directly. -func NewKey(name string, opts ...KeyOption) Key { - kopts := make([]keys.Option, 0, len(opts)) - for _, opt := range opts { - if opt == nil { - continue - } - o := opt - kopts = append(kopts, func(_ byte, b []byte) []byte { - return o(b) - }) - } - - return keys.New(name, '.', kopts...) -} - -// KeyFactory returns a function that creates Keys with the specified -// segments. The returned function accepts a base name and produces keys of the form: -// -// segment1.segment2....name -// -// Empty segments are skipped, and if both segments and name are -// empty, the resulting Key is "". -// -// For example: -// -// databaseUserKeyFactory := KeyFactory(WithSegments("database", "user")) -// databaseUserIDKey := databaseUserKeyFactory("id") -// // databaseUserIDKey == "database.user.id" -// -// Deprecated: use github.com/ygrebnov/keys.Factory with separator '.' directly. -func KeyFactory(opts ...KeyOption) func(name string) Key { - return func(name string) Key { - return NewKey(name, opts...) - } -} diff --git a/key_test.go b/key_test.go deleted file mode 100644 index 5b157dc..0000000 --- a/key_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package errorc - -import "testing" - -func TestNewKey(t *testing.T) { - tests := []struct { - name string - base string - opts []KeyOption - want Key - }{ - { - name: "no options, non-empty base", - base: "field1", - want: Key("field1"), - }, - { - name: "no options, empty base", - base: "", - want: Key(""), - }, - { - name: "two segments, non-empty base", - base: "field", - opts: []KeyOption{WithSegments("one", "two")}, - want: Key("one.two.field"), - }, - { - name: "empty segment skipped", - base: "field", - opts: []KeyOption{WithSegments("", "x")}, - want: Key("x.field"), - }, - { - name: "options with empty segments only", - base: "", - opts: []KeyOption{WithSegments("", "")}, - want: Key(""), - }, - { - name: "options with empty segments only and non-empty base", - base: "field", - opts: []KeyOption{WithSegments("", "")}, - want: Key("field"), - }, - { - name: "mixed empty/non-empty segments, empty base", - base: "", - opts: []KeyOption{WithSegments("", "one", "", "two", "")}, - want: Key("one.two"), - }, - { - name: "mixed empty/non-empty segments, non-empty base", - base: "field", - opts: []KeyOption{WithSegments("", "one", "", "two", "")}, - want: Key("one.two.field"), - }, - { - name: "WithSegments() with zero arguments", - base: "field", - opts: []KeyOption{WithSegments()}, - want: Key("field"), - }, - { - name: "multiple KeyOption combinations", - base: "field", - opts: []KeyOption{WithSegments("one"), WithSegments("two", "three")}, - want: Key("one.two.three.field"), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - got := NewKey(tt.base, tt.opts...) - if got != tt.want { - t.Fatalf("NewKey(%q, ...) = %q, want %q", tt.base, got, tt.want) - } - }) - } -} - -func TestKeyFactory(t *testing.T) { - tests := []struct { - name string - base string - segments []KeySegment - want Key - }{ - { - name: "single segment", - base: "field", - segments: []KeySegment{"user"}, - want: Key("user.field"), - }, - { - name: "multiple segments", - base: "field", - segments: []KeySegment{"user", "id"}, - want: Key("user.id.field"), - }, - { - name: "empty segments are skipped", - base: "field", - segments: []KeySegment{"", "user", ""}, - want: Key("user.field"), - }, - { - name: "no segments", - base: "field", - want: Key("field"), - }, - { - name: "empty base name with segments", - base: "", - segments: []KeySegment{"user", "id"}, - want: Key("user.id"), - }, - { - name: "empty everything yields empty key", - base: "", - segments: nil, - want: Key(""), - }, - { - name: "segments all empty, empty base", - base: "", - segments: []KeySegment{"", ""}, - want: Key(""), - }, - { - name: "segments all empty, non-empty base", - base: "field", - segments: []KeySegment{"", ""}, - want: Key("field"), - }, - { - name: "mixed empty/non-empty segments, empty base", - base: "", - segments: []KeySegment{"", "user", "", "id", ""}, - want: Key("user.id"), - }, - { - name: "mixed empty/non-empty segments, non-empty base", - base: "field", - segments: []KeySegment{"", "user", "", "id", ""}, - want: Key("user.id.field"), - }, - { - name: "WithSegments() with zero arguments", - base: "field", - segments: []KeySegment{}, - want: Key("field"), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - factory := KeyFactory(WithSegments(tt.segments...)) - got := factory(tt.base) - if got != tt.want { - t.Fatalf("KeyFactory(%v)(%q) = %q, want %q", tt.base, tt.segments, got, tt.want) - } - }) - } -} - -func TestKeyFactory_MultipleOptions(t *testing.T) { - tests := []struct { - name string - base string - opts []KeyOption - want Key - }{ - { - name: "multiple KeyOption combinations", - base: "field", - opts: []KeyOption{WithSegments("one"), WithSegments("two", "three")}, - want: Key("one.two.three.field"), - }, - { - name: "multiple KeyOption combinations with empty segments", - base: "field", - opts: []KeyOption{WithSegments(""), WithSegments("one", ""), WithSegments("two")}, - want: Key("one.two.field"), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - factory := KeyFactory(tt.opts...) - got := factory(tt.base) - if got != tt.want { - t.Fatalf("KeyFactory(opts...)(%q) = %q, want %q", tt.base, got, tt.want) - } - }) - } -}