mirror of
https://github.com/aykhans/go-utils.git
synced 2025-10-16 02:35:58 +00:00
first commit
This commit is contained in:
23
.github/workflows/lint.yaml
vendored
Normal file
23
.github/workflows/lint.yaml
vendored
Normal 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
21
.github/workflows/test.yaml
vendored
Normal 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
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bin/*
|
102
.golangci.yaml
Normal file
102
.golangci.yaml
Normal 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
21
LICENSE
Normal 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
147
README.md
Normal 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
41
Taskfile.yaml
Normal 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
5
common/convert.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
func ToPtr[T any](value T) *T {
|
||||||
|
return &value
|
||||||
|
}
|
146
errors/handler.go
Normal file
146
errors/handler.go
Normal 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
384
errors/handler_test.go
Normal 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
14
go.mod
Normal 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
23
go.sum
Normal 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
38
maps/base.go
Normal 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
317
maps/base_test.go
Normal 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
186
parser/string.go
Normal 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
528
parser/string_test.go
Normal 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
92
slice/slice.go
Normal 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
324
slice/slice_test.go
Normal 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")
|
||||||
|
})
|
||||||
|
}
|
Reference in New Issue
Block a user