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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.aider*
2 changes: 1 addition & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ go get github.com/freeformz/sets
* `NewSyncMap()` -> sync.Map based (concurrency safe);
* `NewOrdered()` -> ordered set (uses a map for indexes and a slice for order);
* `NewLockedOrdered()` -> ordered set that is concurrency safe.
* `set` package functions align with standard lib packages like `slices` and `maps`.
* `sets` package functions align with standard lib packages like `slices` and `maps`.
* Implement as much as possible as package functions, not Set methods.
* Exhaustive unit tests via [rapid](https://github.com/flyingmutant/rapid).
* Somewhat exhaustive examples.
Expand Down
20 changes: 20 additions & 0 deletions locked.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,23 @@ func (s *Locked[M]) UnmarshalJSON(d []byte) error {

return nil
}

// Scan implements the sql.Scanner interface. It scans the value from the database into the set. It expects a JSON array
// of the elements in the set. If the JSON is invalid an error is returned. If the value is nil an empty set is
// returned.
func (s *Locked[M]) Scan(src any) error {
s.Lock()
defer s.Unlock()

if s.set == nil {
s.set = New[M]()
}

return scanValue[M](src, s.set.Clear, func(data []byte) error {
um, ok := s.set.(json.Unmarshaler)
if !ok {
return fmt.Errorf("cannot unmarshal set of type %T - not json.Unmarshaler", s.set)
}
return um.UnmarshalJSON(data)
})
}
20 changes: 20 additions & 0 deletions locked_ordered.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,23 @@ func (s *LockedOrdered[M]) UnmarshalJSON(d []byte) error {
}
return nil
}

// Scan implements the sql.Scanner interface. It scans the value from the database into the set. It expects a JSON array
// of the elements in the set. If the JSON is invalid an error is returned. If the value is nil an empty set is
// returned.
func (s *LockedOrdered[M]) Scan(src any) error {
s.Lock()
defer s.Unlock()

if s.set == nil {
s.set = NewOrdered[M]()
}

return scanValue[M](src, s.set.Clear, func(data []byte) error {
um, ok := s.set.(json.Unmarshaler)
if !ok {
return fmt.Errorf("cannot unmarshal set of type %T - not json.Unmarshaler", s.set)
}
return um.UnmarshalJSON(data)
})
}
29 changes: 26 additions & 3 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ func (s *Map[M]) Contains(m M) bool {

// Clear the set and returns the number of elements removed.
func (s *Map[M]) Clear() int {
if s.set == nil {
s.set = make(map[M]struct{})
}
Comment on lines +48 to +50
Copy link

Copilot AI Jun 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Initializing the map in Clear isn’t strictly needed—len(nil) is 0 and delete on a nil map is a no-op. Consider removing the init here and relying on the constructor to handle map allocation.

Suggested change
if s.set == nil {
s.set = make(map[M]struct{})
}

Copilot uses AI. Check for mistakes.
n := len(s.set)
for k := range s.set {
delete(s.set, k)
Expand Down Expand Up @@ -134,12 +137,32 @@ func (s *Map[M]) UnmarshalJSON(d []byte) error {
}

s.Clear()
if s.set == nil {
s.set = make(map[M]struct{})
}
for _, m := range um {
s.Add(m)
}

return nil
}

// scanValue is a helper function that implements the common logic for scanning values into sets.
// It handles nil, []byte, and string types, delegating to the provided unmarshal function.
func scanValue[M comparable](src any, clear func() int, unmarshal func([]byte) error) error {
Copy link

Copilot AI Jun 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The clear callback returns an int that scanValue ignores. Consider changing it to a func() signature to avoid confusion over the unused return value.

Copilot uses AI. Check for mistakes.
switch st := src.(type) {
case nil:
clear()
return nil
case []byte:
return unmarshal(st)
case string:
return unmarshal([]byte(st))
default:
return fmt.Errorf("cannot scan set of type %T - not []byte or string", st)
}
}

// Scan implements the sql.Scanner interface. It scans the value from the database into the set. It expects a JSON array
// of the elements in the set. If the JSON is invalid an error is returned. If the value is nil an empty set is
// returned.
func (s *Map[M]) Scan(src any) error {
return scanValue[M](src, s.Clear, s.UnmarshalJSON)
}
Loading
Loading