Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
with:
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v7
with:
version: v1.55.2
version: v2.0.2
- name: go mod tidy check
uses: katexochen/go-tidy-check@v2
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ This Source Code Form is subject to the terms of the Apache Public License, vers
- Resize queue length
- Dequeue work in queue
- View work in queue and it's current state, priority and position in queue.
- Publication package provides a generic publish-subscribe (pub/sub) mechanism for Go applications. It allows you to create publications to which multiple subscribers can listen. When a message is published, it's distributed to all relevant subscribers.
- **Generic:** Supports publishing and subscribing to messages of any type.
- **Filtering:** Subscribers can define filters to receive only messages that meet specific criteria.
- **Buffered Channels:** Subscribers receive messages through buffered channels, allowing for asynchronous message handling.
- **Concurrency-Safe:** Designed for concurrent use, ensuring safe message delivery in multi-threaded environments.
- **Timeout Support:** Control the maximum time spent attempting to deliver a message to a subscriber.
- **Clean Shutdown:** Gracefully close publications and subscriber channels.
- **Unsubscribe:** Subscribers can unsubscribe from a publication.
- ValidationError
- Facilitates error reflecting validation issues to a user.
- Supports warnings and errors
Expand All @@ -39,6 +47,8 @@ This Source Code Form is subject to the terms of the Apache Public License, vers
- A starting point for setting up and managing HTTP and/or gRPC services.
- Serve both HTTP and gRPC services from single server.
- Ability to configuration via configuration builder with chaining.
- Generic package contains standard library types that are not yet type safe using generics and wraps them in types that are type safe using generics.
- SyncMap wraps a sync.Map with a generic.
- Additional tools to come!

## Contribution
Expand Down
102 changes: 102 additions & 0 deletions generic/syncmap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2025 by Randy Bell. All rights reserved.
*
* This Source Code Form is subject to the terms of the Apache Public License, version 2.0. If a copy of the APL was not distributed with this file, you can obtain one at https://www.apache.org/licenses/LICENSE-2.0.txt.
*/

package generic

import (
"iter"
"sync"
)

// SyncMap is a generic wrapper around sync.Map that provides type safety
type SyncMap[K comparable, V any] struct {
*sync.Map
}

func NewSyncMap[K comparable, V any]() *SyncMap[K, V] {
return &SyncMap[K, V]{
Map: &sync.Map{},
}
}

// Load returns the value stored in the map for a key, or the zero value if no
// value is present. The ok result indicates whether value was found in the map.
func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) {
v, ok := m.Map.Load(key)
if !ok {
return
}

return v.(V), ok
}

// Store sets the value for a key.
func (m *SyncMap[K, V]) Store(key K, value V) {
m.Map.Store(key, value)
}

// Swap swaps the value for a key and returns the previous value if any. The loaded result reports whether the key was present.
func (m *SyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) {
v, l := m.Map.Swap(key, value)
if !l {
return
}
return v.(V), l
}

// Delete deletes the value for a key.
func (m *SyncMap[K, V]) Delete(key K) {
m.Map.Delete(key)
}

// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value. The loaded result is true if the value was loaded, false if stored.
func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
v, l := m.Map.LoadOrStore(key, value)
return v.(V), l
}

// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
v, l := m.Map.LoadAndDelete(key)
if !l {
return
}
return v.(V), l
}

// CompareAndDelete deletes the entry for key if its value is equal to old.
// The old value must be of a comparable type.
func (m *SyncMap[K, V]) CompareAndDelete(key K, old V) bool {
return m.Map.CompareAndDelete(key, old)
}

// CompareAndSwap swaps the old and new values for key if the value stored in the map is equal to old.
func (m *SyncMap[K, V]) CompareAndSwap(key K, old V, new V) bool {
return m.Map.CompareAndSwap(key, old, new)
}

// Range calls f sequentially for each key and value present in the map. If f returns false, range stops the iteration.
func (m *SyncMap[K, V]) Range(f func(key K, value V) bool) {
m.Map.Range(func(k any, v any) bool {
return f(k.(K), v.(V))
})
}

// Iterate returns an iterator that can be used to iterate over the map.
func (m *SyncMap[K, V]) Iterate() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for anyKey, anyValue := range m.Map.Range {
if !yield(
anyKey.(K),
anyValue.(V),
) {
break
}
}
}
}
179 changes: 179 additions & 0 deletions generic/syncmap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* Copyright (c) 2025 by Randy Bell. All rights reserved.
*
* This Source Code Form is subject to the terms of the Apache Public License, version 2.0. If a copy of the APL was not distributed with this file, you can obtain one at https://www.apache.org/licenses/LICENSE-2.0.txt.
*/

package generic

import (
"fmt"
"sync"
"testing"

"github.com/stretchr/testify/assert"
)

func TestSyncMap_Load(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
m.Store("key1", 1)
value, ok := m.Load("key1")
assert.True(t, ok)
assert.Equal(t, 1, value)

value, ok = m.Load("key2")
assert.False(t, ok)
assert.Equal(t, 0, value)
}

func TestSyncMap_Store(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
m.Store("key1", 1)
value, ok := m.Load("key1")
assert.True(t, ok)
assert.Equal(t, 1, value)

m.Store("key1", 2)
value, ok = m.Load("key1")
assert.True(t, ok)
assert.Equal(t, 2, value)
}

func TestSyncMap_Swap(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
m.Store("key1", 1)
previous, loaded := m.Swap("key1", 2)
assert.True(t, loaded)
assert.Equal(t, 1, previous)

value, ok := m.Load("key1")
assert.True(t, ok)
assert.Equal(t, 2, value)

previous, loaded = m.Swap("key2", 3)
assert.False(t, loaded)
assert.Equal(t, 0, previous)

value, ok = m.Load("key2")
assert.True(t, ok)
assert.Equal(t, 3, value)
}

func TestSyncMap_Delete(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
m.Store("key1", 1)
m.Delete("key1")
_, ok := m.Load("key1")
assert.False(t, ok)
}

func TestSyncMap_LoadOrStore(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
actual, loaded := m.LoadOrStore("key1", 1)
assert.False(t, loaded)
assert.Equal(t, 1, actual)

actual, loaded = m.LoadOrStore("key1", 2)
assert.True(t, loaded)
assert.Equal(t, 1, actual)
}

func TestSyncMap_LoadAndDelete(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
m.Store("key1", 1)
value, loaded := m.LoadAndDelete("key1")
assert.True(t, loaded)
assert.Equal(t, 1, value)

_, ok := m.Load("key1")
assert.False(t, ok)

value, loaded = m.LoadAndDelete("key2")
assert.False(t, loaded)
assert.Equal(t, 0, value)
}

func TestSyncMap_CompareAndDelete(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
m.Store("key1", 1)
deleted := m.CompareAndDelete("key1", 1)
assert.True(t, deleted)
_, ok := m.Load("key1")
assert.False(t, ok)

m.Store("key2", 2)
deleted = m.CompareAndDelete("key2", 3)
assert.False(t, deleted)
value, ok := m.Load("key2")
assert.True(t, ok)
assert.Equal(t, 2, value)
}

func TestSyncMap_CompareAndSwap(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
m.Store("key1", 1)
swapped := m.CompareAndSwap("key1", 1, 2)
assert.True(t, swapped)
value, ok := m.Load("key1")
assert.True(t, ok)
assert.Equal(t, 2, value)

swapped = m.CompareAndSwap("key1", 2, 2)
assert.True(t, swapped)
value, ok = m.Load("key1")
assert.True(t, ok)
assert.Equal(t, 2, value)
}

func TestSyncMap_Range(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
m.Store("key1", 1)
m.Store("key2", 2)
m.Store("key3", 3)

count := 0
m.Range(func(key string, value int) bool {
count++
assert.Contains(t, []string{"key1", "key2", "key3"}, key)
assert.Contains(t, []int{1, 2, 3}, value)
return true
})
assert.Equal(t, 3, count)

count = 0
m.Range(func(key string, value int) bool {
count++
return key != "key2"
})
assert.Equal(t, 2, count)
}

func TestSyncMap_Iterate(t *testing.T) {
m := SyncMap[string, int]{Map: &sync.Map{}}
m.Store("key1", 1)
m.Store("key2", 2)
m.Store("key3", 3)

count := 0
it := m.Iterate()
it(func(key string, value int) bool {
count++
assert.Contains(t, []string{"key1", "key2", "key3"}, key)
assert.Contains(t, []int{1, 2, 3}, value)
return true
})
assert.Equal(t, 3, count)

brokeEarly := false
fmt.Println("test")
it = m.Iterate()
it(func(key string, value int) bool {
count++
if key == "key2" {
brokeEarly = true
return false
}
return true
})
assert.True(t, brokeEarly)
}
24 changes: 12 additions & 12 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
module github.com/rbell/toolchest

go 1.22.1

toolchain go1.22.2
go 1.23.7

require (
github.com/google/btree v1.1.2
github.com/google/btree v1.1.3
github.com/google/uuid v1.6.0
github.com/richardwilkes/toolbox v1.112.0
github.com/stretchr/testify v1.9.0
google.golang.org/grpc v1.63.2
google.golang.org/protobuf v1.33.1-0.20240408130810-98873a205002
github.com/richardwilkes/toolbox v1.122.1
github.com/stretchr/testify v1.10.0
google.golang.org/grpc v1.71.0
google.golang.org/protobuf v1.36.5
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.7.0 // 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
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading