first commit

This commit is contained in:
2025-10-11 19:53:28 +04:00
commit 0d53670ab7
18 changed files with 2413 additions and 0 deletions

23
.github/workflows/lint.yaml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: golangci-lint
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.5.0

21
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Tests
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run tests
run: go test -v -race ./...

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
bin/*

102
.golangci.yaml Normal file
View File

@@ -0,0 +1,102 @@
version: "2"
run:
go: "1.25"
concurrency: 12
linters:
default: none
enable:
- asciicheck
- errcheck
- govet
- ineffassign
- misspell
- nakedret
- nolintlint
- prealloc
- reassign
- staticcheck
- unconvert
- unused
- whitespace
- bidichk
- bodyclose
- containedctx
- contextcheck
- copyloopvar
- embeddedstructfieldcheck
- errchkjson
- errorlint
- exptostd
- fatcontext
- forcetypeassert
- funcorder
- gocheckcompilerdirectives
- gocritic
- gomoddirectives
- gosec
- gosmopolitan
- grouper
- importas
- inamedparam
- intrange
- loggercheck
- mirror
- musttag
- perfsprint
- predeclared
- tagalign
- tagliatelle
- testifylint
- thelper
- tparallel
- unparam
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
settings:
staticcheck:
checks:
- "all"
- "-S1002"
- "-ST1000"
varnamelen:
ignore-decls:
- w http.ResponseWriter
exclusions:
rules:
- path: _test\.go$
linters:
- errorlint
- forcetypeassert
- perfsprint
- errcheck
- gosec
- varnamelen
- path: _test\.go$
linters:
- staticcheck
text: "SA5011"
formatters:
enable:
- gofmt
settings:
gofmt:
# Simplify code: gofmt with `-s` option.
# Default: true
simplify: true
# Apply the rewrite rules to the source before reformatting.
# https://pkg.go.dev/cmd/gofmt
# Default: []
rewrite-rules:
- pattern: "interface{}"
replacement: "any"
- pattern: "a[b:len(a)]"
replacement: "a[b:]"

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Aykhan Shahsuvarov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

147
README.md Normal file
View File

@@ -0,0 +1,147 @@
# go-utils
A collection of generic utility functions for Go projects.
## Installation
```bash
go get github.com/aykhans/go-utils
```
## Packages
### common
Generic type conversion utilities.
**ToPtr** - Convert any value to a pointer
```go
import "github.com/aykhans/go-utils/common"
num := 42
ptr := common.ToPtr(num) // *int
```
### parser
String parsing utilities with generic type support.
**ParseString** - Parse string to various types
```go
import "github.com/aykhans/go-utils/parser"
num, err := parser.ParseString[int]("42")
duration, err := parser.ParseString[time.Duration]("5s")
isValid, err := parser.ParseString[bool]("true")
```
**ParseStringOrZero** - Parse string or return zero value on error
```go
num := parser.ParseStringOrZero[int]("invalid") // returns 0
```
**ParseStringWithDefault** - Parse string with default value fallback
```go
num, err := parser.ParseStringWithDefault("invalid", 10) // returns 10, error
```
**ParseStringOrDefault** - Parse string or return default value without error
```go
num := parser.ParseStringOrDefault("invalid", 10) // returns 10
```
Supported types: `string`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `float64`, `bool`, `time.Duration`, `url.URL`
### slice
Slice manipulation utilities.
**Cycle** - Create an infinite cycler through items
```go
import "github.com/aykhans/go-utils/slice"
next := slice.Cycle(1, 2, 3)
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3
fmt.Println(next()) // 1
```
**RandomCycle** - Cycle through items with randomization
```go
next := slice.RandomCycle(nil, "a", "b", "c")
// Cycles through all items, then starts from a random position
```
### maps
Map utility functions.
**InitMap** - Initialize a map pointer if nil
```go
import "github.com/aykhans/go-utils/maps"
var m map[string]int
maps.InitMap(&m)
m["key"] = 42 // safe to use
```
**UpdateMap** - Merge entries from one map into another
```go
old := map[string]int{"a": 1, "b": 2}
new := map[string]int{"b": 3, "c": 4}
maps.UpdateMap(&old, new)
// old is now: {"a": 1, "b": 3, "c": 4}
```
### errors
Advanced error handling utilities.
**HandleError** - Process errors with custom matchers
```go
import "github.com/aykhans/go-utils/errors"
handled, result := errors.HandleError(err,
errors.OnSentinelError(io.EOF, func(e error) error {
return nil // EOF is expected, ignore it
}),
errors.OnCustomError(func(e *CustomError) error {
return fmt.Errorf("custom error: %w", e)
}),
)
```
**HandleErrorOrDie** - Handle errors or panic if unhandled
```go
result := errors.HandleErrorOrDie(err,
errors.OnSentinelError(context.Canceled, func(e error) error {
return fmt.Errorf("operation canceled")
}),
) // Panics if err doesn't match any handler
```
**OnSentinelError** - Create matcher for sentinel errors (like `io.EOF`)
```go
matcher := errors.OnSentinelError(io.EOF, func(e error) error {
log.Println("reached end of file")
return nil
})
```
**OnCustomError** - Create matcher for custom error types
```go
type ValidationError struct {
Field string
Msg string
}
matcher := errors.OnCustomError(func(e *ValidationError) error {
log.Printf("validation failed on field %s", e.Field)
return fmt.Errorf("invalid input: %w", e)
})
```
## Requirements
- Go 1.25.0 or higher

41
Taskfile.yaml Normal file
View File

@@ -0,0 +1,41 @@
# https://taskfile.dev
version: "3"
vars:
BIN_DIR: ./bin
GOLANGCI_LINT_VERSION: v2.5.0
tasks:
ftl:
cmds:
- task: fmt
- task: tidy
- task: lint
tidy: go mod tidy {{.CLI_ARGS}}
test: go test ./... {{.CLI_ARGS}}
fmt:
desc: Run linters
deps:
- install-golangci-lint
cmds:
- "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}} fmt"
lint:
desc: Run linters
deps:
- install-golangci-lint
cmds:
- "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}} run"
install-golangci-lint:
desc: Install golangci-lint
status:
- test -f {{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}
cmds:
- mkdir -p {{.BIN_DIR}}
- rm -f {{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b {{.BIN_DIR}} {{.GOLANGCI_LINT_VERSION}}
- mv {{.BIN_DIR}}/golangci-lint {{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}

5
common/convert.go Normal file
View File

@@ -0,0 +1,5 @@
package common
func ToPtr[T any](value T) *T {
return &value
}

146
errors/handler.go Normal file
View File

@@ -0,0 +1,146 @@
package errors
import (
"errors"
"fmt"
"reflect"
)
// ErrorHandler represents a function that handles a specific error type
type ErrorHandler func(error) error
// ErrorMatcher holds the error type/value and its handler
type ErrorMatcher struct {
ErrorType any // Can be error value (sentinel) or error type
Handler ErrorHandler
IsSentinel bool // true for sentinel errors, false for custom types
}
// HandleError processes an error against a list of matchers and executes the appropriate handler.
// It returns (true, handlerResult) if a matching handler is found and executed,
// or (false, nil) if no matcher matches the error.
// If err is nil, returns (true, nil).
//
// Example:
//
// handled, result := HandleError(err,
// OnSentinelError(io.EOF, func(e error) error {
// return nil // EOF is expected, ignore it
// }),
// OnCustomError(func(e *CustomError) error {
// return fmt.Errorf("custom error: %w", e)
// }),
// )
func HandleError(err error, matchers ...ErrorMatcher) (bool, error) {
if err == nil {
return true, nil
}
for _, matcher := range matchers {
if matcher.IsSentinel {
// Handle sentinel errors with errors.Is
if sentinelErr, ok := matcher.ErrorType.(error); ok {
if errors.Is(err, sentinelErr) {
return true, matcher.Handler(err)
}
}
} else {
// Handle custom error types with errors.As
errorType := reflect.TypeOf(matcher.ErrorType)
errorValue := reflect.New(errorType).Interface()
if errors.As(err, errorValue) {
return true, matcher.Handler(err)
}
}
}
return false, err // No matcher found
}
// HandleErrorOrDie processes an error against a list of matchers and executes the appropriate handler.
// If a matching handler is found, it returns the handler's result.
// If no matcher matches the error, it panics with a descriptive message.
// This function is useful when all expected error types must be handled explicitly.
//
// Example:
//
// result := HandleErrorOrDie(err,
// OnSentinelError(context.Canceled, func(e error) error {
// return fmt.Errorf("operation canceled")
// }),
// OnCustomError(func(e *ValidationError) error {
// return fmt.Errorf("validation failed: %w", e)
// }),
// ) // Panics if err doesn't match any handler
func HandleErrorOrDie(err error, matchers ...ErrorMatcher) error {
ok, err := HandleError(err, matchers...)
if !ok {
panic(fmt.Sprintf("Unhandled error of type %T: %v", err, err))
}
return err
}
// OnSentinelError creates an ErrorMatcher for sentinel errors.
// Sentinel errors are predefined error values that are compared using errors.Is.
//
// This is used with HandleError or HandleErrorOrDie to match specific error
// values like io.EOF, context.Canceled, or custom sentinel errors defined with
// errors.New or fmt.Errorf.
//
// The handler function receives the original error and can return a new error
// or nil to suppress it.
//
// Example:
//
// matcher := OnSentinelError(io.EOF, func(e error) error {
// log.Println("reached end of file")
// return nil // suppress EOF error
// })
func OnSentinelError(sentinelErr error, handler ErrorHandler) ErrorMatcher {
return ErrorMatcher{
ErrorType: sentinelErr,
Handler: handler,
IsSentinel: true,
}
}
// OnCustomError creates an ErrorMatcher for custom error types.
// Custom error types are struct types that implement the error interface,
// and are matched using errors.As to unwrap error chains.
//
// The type parameter T specifies the error type to match. The handler function
// receives the unwrapped typed error, allowing you to access type-specific fields
// and methods.
//
// This is particularly useful for handling errors with additional context or data,
// such as validation errors, network errors, or domain-specific errors.
//
// Example:
//
// type ValidationError struct {
// Field string
// Msg string
// }
// func (e *ValidationError) Error() string {
// return fmt.Sprintf("%s: %s", e.Field, e.Msg)
// }
//
// matcher := OnCustomError(func(e *ValidationError) error {
// log.Printf("validation failed on field %s: %s", e.Field, e.Msg)
// return fmt.Errorf("invalid input: %w", e)
// })
func OnCustomError[T error](handler func(T) error) ErrorMatcher {
var zero T
return ErrorMatcher{
ErrorType: zero,
Handler: func(err error) error {
var typedErr T
if errors.As(err, &typedErr) {
return handler(typedErr)
}
return nil
},
IsSentinel: false,
}
}

384
errors/handler_test.go Normal file
View File

@@ -0,0 +1,384 @@
package errors
import (
"context"
"errors"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Custom error types for testing
type CustomError struct {
Code int
Message string
}
func (e CustomError) Error() string {
return fmt.Sprintf("custom error %d: %s", e.Code, e.Message)
}
type ValidationError struct {
Field string
Value string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed for field %s with value %s", e.Field, e.Value)
}
// Sentinel errors for testing
var (
ErrSentinel1 = errors.New("sentinel error 1")
ErrSentinel2 = errors.New("sentinel error 2")
)
func TestHandleError(t *testing.T) {
t.Run("HandleError with nil error", func(t *testing.T) {
handled, result := HandleError(nil)
assert.True(t, handled)
assert.NoError(t, result)
})
t.Run("HandleError with sentinel error match", func(t *testing.T) {
err := io.EOF
handled, result := HandleError(err,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("handled EOF")
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "handled EOF")
})
t.Run("HandleError with wrapped sentinel error", func(t *testing.T) {
wrappedErr := fmt.Errorf("wrapped: %w", io.EOF)
handled, result := HandleError(wrappedErr,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("handled wrapped EOF")
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "handled wrapped EOF")
})
t.Run("HandleError with custom error type match", func(t *testing.T) {
err := &CustomError{Code: 404, Message: "not found"}
handled, result := HandleError(err,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("handled custom error with code %d", e.Code)
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "handled custom error with code 404")
})
t.Run("HandleError with wrapped custom error", func(t *testing.T) {
customErr := &CustomError{Code: 500, Message: "internal error"}
wrappedErr := fmt.Errorf("wrapped: %w", customErr)
handled, result := HandleError(wrappedErr,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("handled wrapped custom error: %s", e.Message)
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "handled wrapped custom error: internal error")
})
t.Run("HandleError with no matching handler", func(t *testing.T) {
err := errors.New("unhandled error")
handled, _ := HandleError(err,
OnSentinelError(io.EOF, func(e error) error {
return nil
}),
OnCustomError(func(e CustomError) error {
return nil
}),
)
assert.False(t, handled)
})
t.Run("HandleError with multiple matchers first match wins", func(t *testing.T) {
err := io.EOF
handled, result := HandleError(err,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("first handler")
}),
OnSentinelError(io.EOF, func(e error) error {
return errors.New("second handler")
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "first handler")
})
t.Run("HandleError with handler returning nil", func(t *testing.T) {
err := io.EOF
handled, result := HandleError(err,
OnSentinelError(io.EOF, func(e error) error {
return nil
}),
)
assert.True(t, handled)
assert.NoError(t, result)
})
t.Run("HandleError with multiple error types", func(t *testing.T) {
customErr := &CustomError{Code: 400, Message: "bad request"}
validationErr := &ValidationError{Field: "email", Value: "invalid"}
// Test CustomError handling
handled1, result1 := HandleError(customErr,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("custom: %d", e.Code)
}),
OnCustomError(func(e *ValidationError) error {
return fmt.Errorf("validation: %s", e.Field)
}),
)
assert.True(t, handled1)
require.EqualError(t, result1, "custom: 400")
// Test ValidationError handling
handled2, result2 := HandleError(validationErr,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("custom: %d", e.Code)
}),
OnCustomError(func(e *ValidationError) error {
return fmt.Errorf("validation: %s", e.Field)
}),
)
assert.True(t, handled2)
assert.EqualError(t, result2, "validation: email")
})
t.Run("HandleError with context errors", func(t *testing.T) {
// Test context.Canceled
handled1, result1 := HandleError(context.Canceled,
OnSentinelError(context.Canceled, func(e error) error {
return errors.New("operation canceled")
}),
)
assert.True(t, handled1)
require.EqualError(t, result1, "operation canceled")
// Test context.DeadlineExceeded
handled2, result2 := HandleError(context.DeadlineExceeded,
OnSentinelError(context.DeadlineExceeded, func(e error) error {
return errors.New("deadline exceeded")
}),
)
assert.True(t, handled2)
assert.EqualError(t, result2, "deadline exceeded")
})
t.Run("HandleError preserves original error in handler", func(t *testing.T) {
originalErr := &CustomError{Code: 403, Message: "forbidden"}
var capturedErr error
handled, _ := HandleError(originalErr,
OnCustomError(func(e *CustomError) error {
capturedErr = e
return nil
}),
)
assert.True(t, handled)
assert.Equal(t, originalErr, capturedErr)
})
}
func TestHandleErrorOrDie(t *testing.T) {
t.Run("HandleErrorOrDie with nil error", func(t *testing.T) {
result := HandleErrorOrDie(nil)
assert.NoError(t, result)
})
t.Run("HandleErrorOrDie with matched error", func(t *testing.T) {
err := io.EOF
result := HandleErrorOrDie(err,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("handled EOF in die")
}),
)
assert.EqualError(t, result, "handled EOF in die")
})
t.Run("HandleErrorOrDie panics on unmatched error", func(t *testing.T) {
err := errors.New("unmatched error")
assert.Panics(t, func() {
HandleErrorOrDie(err,
OnSentinelError(io.EOF, func(e error) error {
return nil
}),
)
})
})
t.Run("HandleErrorOrDie with custom error panic", func(t *testing.T) {
customErr := &CustomError{Code: 500, Message: "server error"}
assert.Panics(t, func() {
HandleErrorOrDie(customErr,
OnCustomError(func(e *ValidationError) error {
return nil
}),
)
})
})
t.Run("HandleErrorOrDie with multiple matchers", func(t *testing.T) {
validationErr := &ValidationError{Field: "username", Value: ""}
result := HandleErrorOrDie(validationErr,
OnSentinelError(io.EOF, func(e error) error {
return errors.New("EOF handler")
}),
OnCustomError(func(e *CustomError) error {
return errors.New("custom handler")
}),
OnCustomError(func(e *ValidationError) error {
return fmt.Errorf("validation handler: field=%s", e.Field)
}),
)
assert.EqualError(t, result, "validation handler: field=username")
})
}
func TestOnSentinelError(t *testing.T) {
t.Run("OnSentinelError creates proper matcher", func(t *testing.T) {
handler := func(e error) error { return e }
matcher := OnSentinelError(io.EOF, handler)
assert.Equal(t, io.EOF, matcher.ErrorType)
assert.True(t, matcher.IsSentinel)
assert.NotNil(t, matcher.Handler)
})
t.Run("OnSentinelError with custom sentinel", func(t *testing.T) {
customSentinel := errors.New("custom sentinel")
callCount := 0
matcher := OnSentinelError(customSentinel, func(e error) error {
callCount++
return errors.New("handled custom sentinel")
})
// Test that it matches the sentinel
handled, result := HandleError(customSentinel, matcher)
assert.True(t, handled)
require.EqualError(t, result, "handled custom sentinel")
assert.Equal(t, 1, callCount)
// Test that it matches wrapped sentinel
wrappedErr := fmt.Errorf("wrapped: %w", customSentinel)
handled, result = HandleError(wrappedErr, matcher)
assert.True(t, handled)
require.EqualError(t, result, "handled custom sentinel")
assert.Equal(t, 2, callCount)
})
}
func TestOnCustomError(t *testing.T) {
t.Run("OnCustomError creates proper matcher", func(t *testing.T) {
matcher := OnCustomError(func(e *CustomError) error {
return fmt.Errorf("handled: %d", e.Code)
})
assert.False(t, matcher.IsSentinel)
assert.NotNil(t, matcher.Handler)
// Test the handler works
err := &CustomError{Code: 200, Message: "ok"}
result := matcher.Handler(err)
assert.EqualError(t, result, "handled: 200")
})
t.Run("OnCustomError with different error types", func(t *testing.T) {
// Create matchers for different types
customMatcher := OnCustomError(func(e *CustomError) error {
return fmt.Errorf("custom error: code=%d", e.Code)
})
validationMatcher := OnCustomError(func(e *ValidationError) error {
return fmt.Errorf("validation error: field=%s", e.Field)
})
// Test with CustomError
customErr := &CustomError{Code: 404, Message: "not found"}
handled, result := HandleError(customErr, customMatcher, validationMatcher)
assert.True(t, handled)
require.EqualError(t, result, "custom error: code=404")
// Test with ValidationError
validationErr := &ValidationError{Field: "age", Value: "-1"}
handled, result = HandleError(validationErr, customMatcher, validationMatcher)
assert.True(t, handled)
assert.EqualError(t, result, "validation error: field=age")
})
t.Run("OnCustomError handler receives correct type", func(t *testing.T) {
var receivedErr *CustomError
matcher := OnCustomError(func(e *CustomError) error {
receivedErr = e
return nil
})
originalErr := &CustomError{Code: 301, Message: "redirect"}
handled, _ := HandleError(originalErr, matcher)
assert.True(t, handled)
require.NotNil(t, receivedErr)
assert.Equal(t, 301, receivedErr.Code)
assert.Equal(t, "redirect", receivedErr.Message)
})
}
func TestErrorMatcherEdgeCases(t *testing.T) {
t.Run("Invalid sentinel error type in matcher", func(t *testing.T) {
// Create a matcher with invalid ErrorType for sentinel
matcher := ErrorMatcher{
ErrorType: "not an error", // Invalid type
Handler: func(e error) error { return e },
IsSentinel: true,
}
err := errors.New("test error")
handled, _ := HandleError(err, matcher)
assert.False(t, handled)
})
t.Run("Handler that panics", func(t *testing.T) {
matcher := OnSentinelError(io.EOF, func(e error) error {
panic("handler panic")
})
assert.Panics(t, func() {
HandleError(io.EOF, matcher)
})
})
t.Run("Complex error chain", func(t *testing.T) {
// Create a complex error chain
baseErr := &CustomError{Code: 500, Message: "base"}
wrapped1 := fmt.Errorf("layer1: %w", baseErr)
wrapped2 := fmt.Errorf("layer2: %w", wrapped1)
wrapped3 := fmt.Errorf("layer3: %w", wrapped2)
handled, result := HandleError(wrapped3,
OnCustomError(func(e *CustomError) error {
return fmt.Errorf("found custom error at code %d", e.Code)
}),
)
assert.True(t, handled)
assert.EqualError(t, result, "found custom error at code 500")
})
}

14
go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/aykhans/go-utils
go 1.25.0
require github.com/stretchr/testify v1.11.1
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

23
go.sum Normal file
View File

@@ -0,0 +1,23 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

38
maps/base.go Normal file
View File

@@ -0,0 +1,38 @@
package maps
import "maps"
// InitMap initializes a map pointer if it is nil.
// If the map is already initialized, this function does nothing.
//
// This is useful for ensuring a map is ready to use before adding entries,
// preventing nil pointer dereferences.
//
// Example:
//
// var m map[string]int
// InitMap(&m)
// m["key"] = 42 // safe to use
func InitMap[K comparable, V any, T ~map[K]V](m *T) {
if *m == nil {
*m = make(T)
}
}
// UpdateMap merges entries from the new map into the old map.
// If the old map is nil, it will be initialized first.
// Existing keys in the old map will be overwritten with values from the new map.
//
// This function modifies the old map in place by copying all key-value pairs
// from the new map into it.
//
// Example:
//
// old := map[string]int{"a": 1, "b": 2}
// new := map[string]int{"b": 3, "c": 4}
// UpdateMap(&old, new)
// // old is now: {"a": 1, "b": 3, "c": 4}
func UpdateMap[K comparable, V any, T ~map[K]V](oldMap *T, newMap T) {
InitMap(oldMap)
maps.Copy(*oldMap, newMap)
}

317
maps/base_test.go Normal file
View File

@@ -0,0 +1,317 @@
package maps
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInitMap(t *testing.T) {
t.Run("initializes nil map", func(t *testing.T) {
var m map[string]int
require.Nil(t, m)
InitMap(&m)
assert.NotNil(t, m)
assert.Empty(t, m)
assert.Empty(t, m)
})
t.Run("does not reinitialize existing map", func(t *testing.T) {
m := map[string]int{"key": 42}
originalLen := len(m)
InitMap(&m)
assert.Len(t, m, originalLen)
assert.Equal(t, 42, m["key"])
})
t.Run("allows adding entries after initialization", func(t *testing.T) {
var m map[string]int
InitMap(&m)
m["test"] = 100
assert.Equal(t, 100, m["test"])
assert.Len(t, m, 1)
})
t.Run("works with empty initialized map", func(t *testing.T) {
m := make(map[string]int)
InitMap(&m)
assert.NotNil(t, m)
assert.Empty(t, m)
})
t.Run("works with different key types", func(t *testing.T) {
t.Run("int keys", func(t *testing.T) {
var m map[int]string
InitMap(&m)
assert.NotNil(t, m)
m[1] = "one"
assert.Equal(t, "one", m[1])
})
t.Run("struct keys", func(t *testing.T) {
type Key struct {
ID int
Name string
}
var m map[Key]bool
InitMap(&m)
assert.NotNil(t, m)
k := Key{ID: 1, Name: "test"}
m[k] = true
assert.True(t, m[k])
})
})
t.Run("works with different value types", func(t *testing.T) {
t.Run("string values", func(t *testing.T) {
var m map[string]string
InitMap(&m)
assert.NotNil(t, m)
m["key"] = "value"
assert.Equal(t, "value", m["key"])
})
t.Run("struct values", func(t *testing.T) {
type Value struct {
Count int
Name string
}
var m map[string]Value
InitMap(&m)
assert.NotNil(t, m)
m["test"] = Value{Count: 5, Name: "foo"}
assert.Equal(t, 5, m["test"].Count)
})
t.Run("slice values", func(t *testing.T) {
var m map[string][]int
InitMap(&m)
assert.NotNil(t, m)
m["numbers"] = []int{1, 2, 3}
assert.Equal(t, []int{1, 2, 3}, m["numbers"])
})
t.Run("pointer values", func(t *testing.T) {
var m map[string]*int
InitMap(&m)
assert.NotNil(t, m)
val := 42
m["ptr"] = &val
assert.Equal(t, 42, *m["ptr"])
})
})
t.Run("works with custom map types", func(t *testing.T) {
type CustomMap map[string]int
var m CustomMap
InitMap(&m)
assert.NotNil(t, m)
m["custom"] = 99
assert.Equal(t, 99, m["custom"])
})
}
func TestUpdateMap(t *testing.T) {
t.Run("merges new entries into existing map", func(t *testing.T) {
oldMap := map[string]int{"a": 1, "b": 2}
newMap := map[string]int{"c": 3, "d": 4}
UpdateMap(&oldMap, newMap)
assert.Len(t, oldMap, 4)
assert.Equal(t, 1, oldMap["a"])
assert.Equal(t, 2, oldMap["b"])
assert.Equal(t, 3, oldMap["c"])
assert.Equal(t, 4, oldMap["d"])
})
t.Run("overwrites existing keys", func(t *testing.T) {
oldMap := map[string]int{"a": 1, "b": 2}
newMap := map[string]int{"b": 3, "c": 4}
UpdateMap(&oldMap, newMap)
assert.Len(t, oldMap, 3)
assert.Equal(t, 1, oldMap["a"])
assert.Equal(t, 3, oldMap["b"], "existing key 'b' should be overwritten")
assert.Equal(t, 4, oldMap["c"])
})
t.Run("initializes nil map before merging", func(t *testing.T) {
var oldMap map[string]int
newMap := map[string]int{"a": 1, "b": 2}
require.Nil(t, oldMap)
UpdateMap(&oldMap, newMap)
assert.NotNil(t, oldMap)
assert.Len(t, oldMap, 2)
assert.Equal(t, 1, oldMap["a"])
assert.Equal(t, 2, oldMap["b"])
})
t.Run("handles empty new map", func(t *testing.T) {
oldMap := map[string]int{"a": 1}
newMap := map[string]int{}
UpdateMap(&oldMap, newMap)
assert.Len(t, oldMap, 1)
assert.Equal(t, 1, oldMap["a"])
})
t.Run("handles empty old map", func(t *testing.T) {
oldMap := map[string]int{}
newMap := map[string]int{"a": 1, "b": 2}
UpdateMap(&oldMap, newMap)
assert.Len(t, oldMap, 2)
assert.Equal(t, 1, oldMap["a"])
assert.Equal(t, 2, oldMap["b"])
})
t.Run("handles both empty maps", func(t *testing.T) {
oldMap := map[string]int{}
newMap := map[string]int{}
UpdateMap(&oldMap, newMap)
assert.NotNil(t, oldMap)
assert.Empty(t, oldMap)
})
t.Run("works with different types", func(t *testing.T) {
t.Run("string values", func(t *testing.T) {
oldMap := map[string]string{"key1": "value1"}
newMap := map[string]string{"key2": "value2"}
UpdateMap(&oldMap, newMap)
assert.Len(t, oldMap, 2)
assert.Equal(t, "value1", oldMap["key1"])
assert.Equal(t, "value2", oldMap["key2"])
})
t.Run("struct values", func(t *testing.T) {
type Person struct {
Name string
Age int
}
oldMap := map[string]Person{
"john": {Name: "John", Age: 30},
}
newMap := map[string]Person{
"jane": {Name: "Jane", Age: 25},
}
UpdateMap(&oldMap, newMap)
assert.Len(t, oldMap, 2)
assert.Equal(t, "John", oldMap["john"].Name)
assert.Equal(t, "Jane", oldMap["jane"].Name)
})
t.Run("int keys", func(t *testing.T) {
oldMap := map[int]string{1: "one"}
newMap := map[int]string{2: "two"}
UpdateMap(&oldMap, newMap)
assert.Len(t, oldMap, 2)
assert.Equal(t, "one", oldMap[1])
assert.Equal(t, "two", oldMap[2])
})
})
t.Run("modifies map in place", func(t *testing.T) {
oldMap := map[string]int{"a": 1}
originalMap := oldMap
newMap := map[string]int{"b": 2}
UpdateMap(&oldMap, newMap)
// Verify it's the same map (modified in place)
assert.Equal(t, 2, oldMap["b"])
// Note: originalMap is pointing to the same underlying map
// so it should also have the new value
assert.Equal(t, 2, originalMap["b"])
})
t.Run("does not modify new map", func(t *testing.T) {
oldMap := map[string]int{"a": 1}
newMap := map[string]int{"b": 2, "c": 3}
newLen := len(newMap)
UpdateMap(&oldMap, newMap)
assert.Len(t, newMap, newLen, "new map should not be modified")
assert.Equal(t, 2, newMap["b"])
assert.Equal(t, 3, newMap["c"])
_, hasA := newMap["a"]
assert.False(t, hasA, "new map should not contain keys from old map")
})
t.Run("works with custom map types", func(t *testing.T) {
type CustomMap map[string]int
oldMap := CustomMap{"a": 1}
newMap := CustomMap{"b": 2}
UpdateMap(&oldMap, newMap)
assert.Len(t, oldMap, 2)
assert.Equal(t, 1, oldMap["a"])
assert.Equal(t, 2, oldMap["b"])
})
t.Run("overwrites with zero values", func(t *testing.T) {
oldMap := map[string]int{"a": 10, "b": 20}
newMap := map[string]int{"a": 0}
UpdateMap(&oldMap, newMap)
assert.Len(t, oldMap, 2)
assert.Equal(t, 0, oldMap["a"], "should overwrite with zero value")
assert.Equal(t, 20, oldMap["b"])
})
t.Run("complex merge scenario", func(t *testing.T) {
oldMap := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
newMap := map[string]int{
"b": 20, // overwrite
"c": 30, // overwrite
"d": 40, // new
"e": 50, // new
}
UpdateMap(&oldMap, newMap)
expected := map[string]int{
"a": 1, // preserved
"b": 20, // overwritten
"c": 30, // overwritten
"d": 40, // new
"e": 50, // new
}
assert.Equal(t, expected, oldMap)
})
}

186
parser/string.go Normal file
View File

@@ -0,0 +1,186 @@
package parser
import (
"fmt"
"net/url"
"strconv"
"time"
)
// ParseStringSupportedTypes defines the type constraint for types that can be
// parsed from strings using the ParseString family of functions.
type ParseStringSupportedTypes interface {
string |
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float64 |
bool | time.Duration | url.URL
}
// ParseString parses a string value into the specified type T.
// It uses the appropriate parsing function based on the target type.
//
// The function supports all types defined in ParseStringSupportedTypes.
// For integers, it parses base-10 numbers with appropriate bit sizes.
// For booleans, it accepts: "1", "t", "T", "TRUE", "true", "True", "0", "f", "F", "FALSE", "false", "False".
// For durations, it accepts strings like "300ms", "1.5h", "2h45m".
// For URLs, it parses according to RFC 3986.
//
// Returns an error if the string cannot be parsed into the target type.
//
// Example:
//
// num, err := ParseString[int]("42")
// duration, err := ParseString[time.Duration]("5s")
// isValid, err := ParseString[bool]("true")
//
//nolint:forcetypeassert
func ParseString[T ParseStringSupportedTypes](rawValue string) (T, error) { //nolint:forcetypeassert
var value T
switch any(value).(type) {
case string:
value = any(rawValue).(T)
case int:
i, err := strconv.Atoi(rawValue)
if err != nil {
return value, err
}
value = any(i).(T)
case int8:
i, err := strconv.ParseInt(rawValue, 10, 8)
if err != nil {
return value, err
}
value = any(int8(i)).(T)
case int16:
i, err := strconv.ParseInt(rawValue, 10, 16)
if err != nil {
return value, err
}
value = any(int16(i)).(T)
case int32:
i, err := strconv.ParseInt(rawValue, 10, 32)
if err != nil {
return value, err
}
value = any(int32(i)).(T)
case int64:
i, err := strconv.ParseInt(rawValue, 10, 64)
if err != nil {
return value, err
}
value = any(i).(T)
case uint:
u, err := strconv.ParseUint(rawValue, 10, 0)
if err != nil {
return value, err
}
value = any(uint(u)).(T)
case uint8:
u, err := strconv.ParseUint(rawValue, 10, 8)
if err != nil {
return value, err
}
value = any(uint8(u)).(T)
case uint16:
u, err := strconv.ParseUint(rawValue, 10, 16)
if err != nil {
return value, err
}
value = any(uint16(u)).(T)
case uint32:
u, err := strconv.ParseUint(rawValue, 10, 32)
if err != nil {
return value, err
}
value = any(uint32(u)).(T)
case uint64:
u, err := strconv.ParseUint(rawValue, 10, 64)
if err != nil {
return value, err
}
value = any(u).(T)
case float64:
f, err := strconv.ParseFloat(rawValue, 64)
if err != nil {
return value, err
}
value = any(f).(T)
case bool:
b, err := strconv.ParseBool(rawValue)
if err != nil {
return value, err
}
value = any(b).(T)
case time.Duration:
d, err := time.ParseDuration(rawValue)
if err != nil {
return value, err
}
value = any(d).(T)
case url.URL:
u, err := url.Parse(rawValue)
if err != nil {
return value, err
}
value = any(*u).(T)
default:
return value, fmt.Errorf("unsupported type: %T", value)
}
return value, nil
}
// ParseStringOrZero parses a string value into the specified type T.
// If parsing fails, it returns the zero value for type T instead of an error.
//
// This is useful when you want a simple conversion that falls back to a
// default zero value on error.
//
// Example:
//
// num := ParseStringOrZero[int]("invalid") // returns 0
// num := ParseStringOrZero[int]("42") // returns 42
func ParseStringOrZero[T ParseStringSupportedTypes](rawValue string) T {
parsedValue, _ := ParseString[T](rawValue)
return parsedValue
}
// ParseStringWithDefault parses a string value into the specified type T.
// If parsing fails, it returns the provided default value along with the error.
//
// Unlike ParseString, this ensures a usable value is always returned,
// but still provides error information for logging or handling.
//
// Example:
//
// num, err := ParseStringWithDefault("invalid", 10)
// // returns: 10, error
// num, err := ParseStringWithDefault("42", 10)
// // returns: 42, nil
func ParseStringWithDefault[T ParseStringSupportedTypes](rawValue string, dft T) (T, error) {
parsedValue, err := ParseString[T](rawValue)
if err != nil {
return dft, err
}
return parsedValue, nil
}
// ParseStringOrDefault parses a string value into the specified type T.
// If parsing fails, it returns the provided default value and suppresses the error.
//
// This is the most convenient option when you want a fallback value
// and don't need to handle the error explicitly.
//
// Example:
//
// num := ParseStringOrDefault("invalid", 10) // returns 10
// num := ParseStringOrDefault("42", 10) // returns 42
func ParseStringOrDefault[T ParseStringSupportedTypes](rawValue string, dft T) T {
parsedValue, err := ParseString[T](rawValue)
if err != nil {
return dft
}
return parsedValue
}

528
parser/string_test.go Normal file
View File

@@ -0,0 +1,528 @@
package parser
import (
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseString(t *testing.T) {
t.Run("ParseString to string", func(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"empty string", "", ""},
{"simple string", "hello", "hello"},
{"string with spaces", "hello world", "hello world"},
{"numeric string", "123", "123"},
{"special characters", "!@#$%^&*()", "!@#$%^&*()"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[string](test.input)
require.NoError(t, err)
assert.Equal(t, test.expected, result)
})
}
})
t.Run("ParseString to int", func(t *testing.T) {
tests := []struct {
name string
input string
expected int
expectError bool
}{
{"positive int", "42", 42, false},
{"negative int", "-42", -42, false},
{"zero", "0", 0, false},
{"invalid int", "abc", 0, true},
{"float string", "3.14", 0, true},
{"empty string", "", 0, true},
{"overflow string", "99999999999999999999", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to int8", func(t *testing.T) {
tests := []struct {
name string
input string
expected int8
expectError bool
}{
{"valid int8", "127", 127, false},
{"min int8", "-128", -128, false},
{"overflow int8", "128", 0, true},
{"underflow int8", "-129", 0, true},
{"invalid", "abc", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int8](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to int16", func(t *testing.T) {
tests := []struct {
name string
input string
expected int16
expectError bool
}{
{"valid int16", "32767", 32767, false},
{"min int16", "-32768", -32768, false},
{"overflow int16", "32768", 0, true},
{"underflow int16", "-32769", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int16](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to int32", func(t *testing.T) {
tests := []struct {
name string
input string
expected int32
expectError bool
}{
{"valid int32", "2147483647", 2147483647, false},
{"min int32", "-2147483648", -2147483648, false},
{"overflow int32", "2147483648", 0, true},
{"underflow int32", "-2147483649", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int32](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to int64", func(t *testing.T) {
tests := []struct {
name string
input string
expected int64
expectError bool
}{
{"valid int64", "9223372036854775807", 9223372036854775807, false},
{"min int64", "-9223372036854775808", -9223372036854775808, false},
{"large number", "123456789012345", 123456789012345, false},
{"invalid", "not a number", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int64](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint
expectError bool
}{
{"valid uint", "42", 42, false},
{"zero", "0", 0, false},
{"large uint", "4294967295", 4294967295, false},
{"negative", "-1", 0, true},
{"invalid", "abc", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint8", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint8
expectError bool
}{
{"valid uint8", "255", 255, false},
{"min uint8", "0", 0, false},
{"overflow uint8", "256", 0, true},
{"negative", "-1", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint8](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint16", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint16
expectError bool
}{
{"valid uint16", "65535", 65535, false},
{"min uint16", "0", 0, false},
{"overflow uint16", "65536", 0, true},
{"negative", "-1", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint16](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint32", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint32
expectError bool
}{
{"valid uint32", "4294967295", 4294967295, false},
{"min uint32", "0", 0, false},
{"overflow uint32", "4294967296", 0, true},
{"negative", "-1", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint32](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint64", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint64
expectError bool
}{
{"valid uint64", "18446744073709551615", 18446744073709551615, false},
{"min uint64", "0", 0, false},
{"large number", "123456789012345", 123456789012345, false},
{"negative", "-1", 0, true},
{"invalid", "not a number", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint64](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to float64", func(t *testing.T) {
tests := []struct {
name string
input string
expected float64
expectError bool
}{
{"integer", "42", 42.0, false},
{"decimal", "3.14159", 3.14159, false},
{"negative", "-2.5", -2.5, false},
{"scientific notation", "1.23e10", 1.23e10, false},
{"zero", "0", 0.0, false},
{"invalid", "not a number", 0, true},
{"empty", "", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[float64](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.InDelta(t, test.expected, result, 0.0001)
}
})
}
})
t.Run("ParseString to bool", func(t *testing.T) {
tests := []struct {
name string
input string
expected bool
expectError bool
}{
{"true lowercase", "true", true, false},
{"True mixed case", "True", true, false},
{"TRUE uppercase", "TRUE", true, false},
{"1 as true", "1", true, false},
{"false lowercase", "false", false, false},
{"False mixed case", "False", false, false},
{"FALSE uppercase", "FALSE", false, false},
{"0 as false", "0", false, false},
{"invalid", "yes", false, true},
{"empty", "", false, true},
{"numeric non-binary", "2", false, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[bool](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to time.Duration", func(t *testing.T) {
tests := []struct {
name string
input string
expected time.Duration
expectError bool
}{
{"seconds", "10s", 10 * time.Second, false},
{"minutes", "5m", 5 * time.Minute, false},
{"hours", "2h", 2 * time.Hour, false},
{"combined", "1h30m45s", time.Hour + 30*time.Minute + 45*time.Second, false},
{"milliseconds", "500ms", 500 * time.Millisecond, false},
{"microseconds", "100us", 100 * time.Microsecond, false},
{"nanoseconds", "50ns", 50 * time.Nanosecond, false},
{"negative", "-5s", -5 * time.Second, false},
{"invalid", "5x", 0, true},
{"empty", "", 0, true},
{"no unit", "100", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[time.Duration](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to url.URL", func(t *testing.T) {
tests := []struct {
name string
input string
checkFunc func(t *testing.T, u url.URL)
expectError bool
}{
{
name: "http URL",
input: "http://example.com",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "http", u.Scheme)
assert.Equal(t, "example.com", u.Host)
},
expectError: false,
},
{
name: "https URL with path",
input: "https://example.com/path/to/resource",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "example.com", u.Host)
assert.Equal(t, "/path/to/resource", u.Path)
},
expectError: false,
},
{
name: "URL with query parameters",
input: "https://example.com/search?q=test&page=1",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "example.com", u.Host)
assert.Equal(t, "/search", u.Path)
assert.Equal(t, "q=test&page=1", u.RawQuery)
},
expectError: false,
},
{
name: "URL with port",
input: "http://localhost:8080/api",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "http", u.Scheme)
assert.Equal(t, "localhost:8080", u.Host)
assert.Equal(t, "/api", u.Path)
},
expectError: false,
},
{
name: "URL with fragment",
input: "https://example.com/page#section",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "example.com", u.Host)
assert.Equal(t, "/page", u.Path)
assert.Equal(t, "section", u.Fragment)
},
expectError: false,
},
{
name: "relative path",
input: "/path/to/resource",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Empty(t, u.Scheme)
assert.Empty(t, u.Host)
assert.Equal(t, "/path/to/resource", u.Path)
},
expectError: false,
},
{
name: "empty string",
input: "",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Empty(t, u.String())
},
expectError: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[url.URL](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
if test.checkFunc != nil {
test.checkFunc(t, result)
}
}
})
}
})
t.Run("Edge cases", func(t *testing.T) {
t.Run("whitespace handling for numeric types", func(t *testing.T) {
result, err := ParseString[int](" 42 ")
require.Error(t, err)
assert.Equal(t, 0, result)
})
t.Run("leading zeros for int", func(t *testing.T) {
result, err := ParseString[int]("007")
require.NoError(t, err)
assert.Equal(t, 7, result)
})
t.Run("plus sign for positive numbers", func(t *testing.T) {
result, err := ParseString[int]("+42")
require.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("case sensitivity for bool", func(t *testing.T) {
testCases := []string{"t", "T", "f", "F"}
for _, tc := range testCases {
result, err := ParseString[bool](tc)
require.NoError(t, err)
if tc == "t" || tc == "T" {
assert.True(t, result)
} else {
assert.False(t, result)
}
}
})
})
}

92
slice/slice.go Normal file
View File

@@ -0,0 +1,92 @@
package slice
import (
"math/rand/v2"
"time"
)
// Cycle returns a function that cycles through the provided items infinitely.
// Each call to the returned function returns the next item in sequence, wrapping
// back to the first item after the last one is returned.
//
// If no items are provided, the returned function will always return the zero
// value for type T.
//
// The returned function is not safe for concurrent use. If you need to call it
// from multiple goroutines, you must synchronize access with a mutex or similar.
//
// Example:
//
// next := Cycle(1, 2, 3)
// fmt.Println(next()) // 1
// fmt.Println(next()) // 2
// fmt.Println(next()) // 3
// fmt.Println(next()) // 1
func Cycle[T any](items ...T) func() T {
if len(items) == 0 {
var zero T
return func() T { return zero }
}
index := 0
return func() T {
item := items[index]
index = (index + 1) % len(items)
return item
}
}
// RandomCycle returns a function that cycles through the provided items with
// randomization. It cycles through all items sequentially, but when it completes
// a full cycle, it randomly picks a new starting point for the next cycle.
//
// The localRand parameter can be used to provide a custom random number generator.
// If nil, a new generator will be created using the current time as the seed.
//
// The returned function is not safe for concurrent use. If you need to call it
// from multiple goroutines, you must synchronize access with a mutex or similar.
//
// Special cases:
// - If no items are provided, the returned function always returns the zero value for type T.
// - If only one item is provided, the returned function always returns that item.
//
// Example:
//
// next := RandomCycle(nil, "a", "b", "c")
// // Might produce: "b", "c", "a", "c", "a", "b", ...
// // (cycles through all items, then starts from a random position)
func RandomCycle[T any](localRand *rand.Rand, items ...T) func() T {
switch sliceLen := len(items); sliceLen {
case 0:
var zero T
return func() T { return zero }
case 1:
return func() T { return items[0] }
default:
if localRand == nil {
//nolint:gosec
localRand = rand.New(
rand.NewPCG(
uint64(time.Now().UnixNano()),
uint64(time.Now().UnixNano()>>32),
),
)
}
currentIndex := localRand.IntN(sliceLen)
stopIndex := currentIndex
return func() T {
item := items[currentIndex]
currentIndex++
if currentIndex == sliceLen {
currentIndex = 0
}
if currentIndex == stopIndex {
currentIndex = localRand.IntN(sliceLen)
stopIndex = currentIndex
}
return item
}
}
}

324
slice/slice_test.go Normal file
View File

@@ -0,0 +1,324 @@
package slice
import (
"math/rand/v2"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCycle(t *testing.T) {
t.Run("cycles through items", func(t *testing.T) {
next := Cycle(1, 2, 3)
assert.Equal(t, 1, next())
assert.Equal(t, 2, next())
assert.Equal(t, 3, next())
assert.Equal(t, 1, next()) // wraps back to first
assert.Equal(t, 2, next())
assert.Equal(t, 3, next())
assert.Equal(t, 1, next()) // wraps again
})
t.Run("cycles through single item", func(t *testing.T) {
next := Cycle(42)
assert.Equal(t, 42, next())
assert.Equal(t, 42, next())
assert.Equal(t, 42, next())
})
t.Run("returns zero value for empty slice", func(t *testing.T) {
next := Cycle[int]()
assert.Equal(t, 0, next())
assert.Equal(t, 0, next())
assert.Equal(t, 0, next())
})
t.Run("works with string type", func(t *testing.T) {
next := Cycle("a", "b", "c")
assert.Equal(t, "a", next())
assert.Equal(t, "b", next())
assert.Equal(t, "c", next())
assert.Equal(t, "a", next())
})
t.Run("works with struct type", func(t *testing.T) {
type Point struct {
X, Y int
}
next := Cycle(
Point{1, 2},
Point{3, 4},
)
assert.Equal(t, Point{1, 2}, next())
assert.Equal(t, Point{3, 4}, next())
assert.Equal(t, Point{1, 2}, next())
})
t.Run("works with pointer type", func(t *testing.T) {
val1, val2 := 10, 20
next := Cycle(&val1, &val2)
assert.Equal(t, &val1, next())
assert.Equal(t, &val2, next())
assert.Equal(t, &val1, next())
})
t.Run("empty string slice returns empty string", func(t *testing.T) {
next := Cycle[string]()
assert.Empty(t, next())
assert.Empty(t, next())
})
t.Run("multiple cycles work correctly", func(t *testing.T) {
next := Cycle("x", "y")
// First cycle
assert.Equal(t, "x", next())
assert.Equal(t, "y", next())
// Second cycle
assert.Equal(t, "x", next())
assert.Equal(t, "y", next())
// Third cycle
assert.Equal(t, "x", next())
assert.Equal(t, "y", next())
})
t.Run("each function instance maintains its own state", func(t *testing.T) {
next1 := Cycle(1, 2, 3)
next2 := Cycle(1, 2, 3)
assert.Equal(t, 1, next1())
assert.Equal(t, 1, next2())
assert.Equal(t, 2, next1())
assert.Equal(t, 2, next2())
assert.Equal(t, 3, next1())
assert.Equal(t, 1, next1())
assert.Equal(t, 3, next2())
})
t.Run("works with boolean type", func(t *testing.T) {
next := Cycle(true, false)
assert.True(t, next())
assert.False(t, next())
assert.True(t, next())
assert.False(t, next())
})
t.Run("works with float type", func(t *testing.T) {
next := Cycle(1.1, 2.2, 3.3)
assert.InDelta(t, 1.1, next(), 0.001)
assert.InDelta(t, 2.2, next(), 0.001)
assert.InDelta(t, 3.3, next(), 0.001)
assert.InDelta(t, 1.1, next(), 0.001)
})
}
func TestRandomCycle(t *testing.T) {
t.Run("returns zero value for empty slice", func(t *testing.T) {
next := RandomCycle[int](nil)
assert.Equal(t, 0, next())
assert.Equal(t, 0, next())
assert.Equal(t, 0, next())
})
t.Run("returns same item for single item slice", func(t *testing.T) {
next := RandomCycle(nil, 42)
assert.Equal(t, 42, next())
assert.Equal(t, 42, next())
assert.Equal(t, 42, next())
})
t.Run("cycles through all items with seeded random", func(t *testing.T) {
seed := rand.NewPCG(1, 2)
r := rand.New(seed)
next := RandomCycle(r, "a", "b", "c")
// Collect items to verify all are returned
seen := make(map[string]bool)
for range 100 {
item := next()
seen[item] = true
}
// All items should have been seen
assert.True(t, seen["a"], "should see 'a'")
assert.True(t, seen["b"], "should see 'b'")
assert.True(t, seen["c"], "should see 'c'")
})
t.Run("works with seeded random generator", func(t *testing.T) {
// Using same seed should produce same sequence
seed1 := rand.NewPCG(42, 42)
r1 := rand.New(seed1)
next1 := RandomCycle(r1, 1, 2, 3)
seed2 := rand.NewPCG(42, 42)
r2 := rand.New(seed2)
next2 := RandomCycle(r2, 1, 2, 3)
// First few calls should match
for range 10 {
assert.Equal(t, next1(), next2(), "calls with same seed should match")
}
})
t.Run("creates own random generator when nil provided", func(t *testing.T) {
next := RandomCycle[int](nil, 1, 2, 3)
// Should not panic and should return valid values
for range 10 {
val := next()
assert.Contains(t, []int{1, 2, 3}, val)
}
})
t.Run("eventually returns all items in cycle", func(t *testing.T) {
seed := rand.NewPCG(123, 456)
r := rand.New(seed)
next := RandomCycle(r, 1, 2, 3, 4, 5)
// Track items seen in current "window"
for range 5 {
seen := make(map[int]int)
// Collect enough items to ensure we see at least one full cycle
for range 10 {
item := next()
seen[item]++
assert.Contains(t, []int{1, 2, 3, 4, 5}, item)
}
// Should see multiple items
assert.GreaterOrEqual(t, len(seen), 3, "should see at least 3 different items")
}
})
t.Run("works with string type", func(t *testing.T) {
seed := rand.NewPCG(1, 2)
r := rand.New(seed)
next := RandomCycle(r, "x", "y", "z")
seen := make(map[string]bool)
for range 50 {
item := next()
seen[item] = true
assert.Contains(t, []string{"x", "y", "z"}, item)
}
assert.True(t, seen["x"])
assert.True(t, seen["y"])
assert.True(t, seen["z"])
})
t.Run("works with struct type", func(t *testing.T) {
type Item struct {
ID int
}
seed := rand.NewPCG(1, 2)
r := rand.New(seed)
next := RandomCycle(r, Item{1}, Item{2}, Item{3})
seen := make(map[int]bool)
for range 50 {
item := next()
seen[item.ID] = true
}
assert.True(t, seen[1])
assert.True(t, seen[2])
assert.True(t, seen[3])
})
t.Run("each function instance maintains its own state", func(t *testing.T) {
seed1 := rand.NewPCG(1, 2)
r1 := rand.New(seed1)
next1 := RandomCycle(r1, 1, 2, 3)
seed2 := rand.NewPCG(3, 5)
r2 := rand.New(seed2)
next2 := RandomCycle(r2, 1, 2, 3)
// Get a few values from each
vals1 := []int{next1(), next1(), next1()}
vals2 := []int{next2(), next2(), next2()}
// They should be different (very high probability with different seeds)
assert.NotEqual(t, vals1, vals2, "different seeds should produce different sequences")
})
t.Run("with two items", func(t *testing.T) {
seed := rand.NewPCG(99, 100)
r := rand.New(seed)
next := RandomCycle(r, "a", "b")
seen := make(map[string]bool)
for range 20 {
item := next()
seen[item] = true
assert.Contains(t, []string{"a", "b"}, item)
}
assert.True(t, seen["a"])
assert.True(t, seen["b"])
})
t.Run("deterministic with same seed across multiple cycles", func(t *testing.T) {
// First run
seed1 := rand.NewPCG(777, 888)
r1 := rand.New(seed1)
next1 := RandomCycle(r1, 10, 20, 30)
sequence1 := make([]int, 20)
for i := range 20 {
sequence1[i] = next1()
}
// Second run with same seed
seed2 := rand.NewPCG(777, 888)
r2 := rand.New(seed2)
next2 := RandomCycle(r2, 10, 20, 30)
sequence2 := make([]int, 20)
for i := range 20 {
sequence2[i] = next2()
}
assert.Equal(t, sequence1, sequence2, "same seed should produce same sequence")
})
t.Run("empty string slice returns empty string", func(t *testing.T) {
next := RandomCycle[string](nil)
assert.Empty(t, next())
assert.Empty(t, next())
})
t.Run("large number of items", func(t *testing.T) {
items := make([]int, 100)
for i := range 100 {
items[i] = i
}
seed := rand.NewPCG(42, 43)
r := rand.New(seed)
next := RandomCycle(r, items...)
seen := make(map[int]bool)
// Call enough times to likely see all items
for range 1000 {
item := next()
seen[item] = true
assert.GreaterOrEqual(t, item, 0)
assert.LessOrEqual(t, item, 99)
}
// Should see a good variety of items
assert.Greater(t, len(seen), 90, "should see most items with 1000 calls")
})
}