Last active
January 20, 2024 22:05
-
-
Save scottcagno/dfe0a83bc2ca94ebf3817c5e62e8d27f to your computer and use it in GitHub Desktop.
Generic Repository idea and implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package api | |
import ( | |
"errors" | |
"sync" | |
) | |
// QueryFunc is a function that is injected with a type | |
// and returns a boolean | |
type QueryFunc[T any] func(t T) bool | |
type FindFunc[T any] func(query QueryFunc[T]) ([]T, error) | |
type ExecFunc[T any] func(query QueryFunc[T], exec QueryFunc[T]) (int, error) | |
type InsertFunc[K comparable, T any] func(newK K, newT T) error | |
type UpdateFunc[K comparable, T any] func(oldK K, newT T) error | |
type DeleteFunc[K comparable] func(oldK K) error | |
// Repository is an interface for representing a generic | |
// data repository | |
type Repository[T any, K comparable] interface { | |
// Find provides the user with an interface for | |
// locating, retrieving or viewing one or more | |
// entries. | |
Find(query QueryFunc[T]) ([]T, error) | |
// Exec provides the user with an interface for | |
// locating and executing a function on the items matching | |
// the query criteria. | |
Exec(query QueryFunc[T], exec QueryFunc[T]) (int, error) | |
// Insert provides the user with an interface for | |
// creating and adding new entries. | |
Insert(newK K, newT T) error | |
// Update provides the user with an interface for | |
// editing or updating an existing entry. | |
Update(oldK K, newT T) error | |
// Delete provides the user with an interface for | |
// removing or invalidating an existing entry. | |
Delete(oldK K) error | |
// Type returns the data type that is used with the Repository | |
Type() T | |
// KeyType returns the data type that is used as a primary key with the Repository | |
KeyType() K | |
} | |
type MemoryRepository[T any, K comparable] struct { | |
lock sync.Mutex | |
isLocked bool | |
data map[K]T | |
} | |
func NewMemoryRepository[T any, K comparable]() *MemoryRepository[T, K] { | |
return &MemoryRepository[T, K]{ | |
data: make(map[K]T), | |
} | |
} | |
func (repo *MemoryRepository[T, K]) Find(query QueryFunc[T]) ([]T, error) { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
if len(repo.data) < 1 { | |
return nil, errors.New("error: cannot find anything because there is no data") | |
} | |
var res []T | |
for k, t := range repo.data { | |
if query(t) { | |
res = append(res, repo.data[k]) | |
} | |
} | |
if len(res) == 0 { | |
return nil, errors.New("error: query did not match anything") | |
} | |
return res, nil | |
} | |
func (repo *MemoryRepository[T, K]) Exec(query QueryFunc[T], exec QueryFunc[T]) (int, error) { | |
var res []T | |
f1 := func() (int, error) { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
if len(repo.data) < 1 { | |
return -1, errors.New("error: cannot find anything because there is no data") | |
} | |
for k, t := range repo.data { | |
if query(t) { | |
res = append(res, repo.data[k]) | |
} | |
} | |
if len(res) == 0 { | |
return 0, errors.New("error: query did not match anything") | |
} | |
return len(res), nil | |
} | |
f2 := func() (int, error) { | |
var ops int | |
for i := range res { | |
if exec(res[i]) { | |
ops++ | |
} | |
} | |
return ops, nil | |
} | |
n, err := f1() | |
if err != nil { | |
return n, err | |
} | |
return f2() | |
} | |
func (repo *MemoryRepository[T, K]) Insert(newK K, newT T) error { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
_, exists := repo.data[newK] | |
if exists { | |
return errors.New("error: cannot insert, item already exists") | |
} | |
repo.data[newK] = newT | |
return nil | |
} | |
func (repo *MemoryRepository[T, K]) Update(oldK K, newT T) error { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
_, exists := repo.data[oldK] | |
if !exists { | |
return errors.New("error: cannot update, item does not exist") | |
} | |
repo.data[oldK] = newT | |
return nil | |
} | |
func (repo *MemoryRepository[T, K]) Delete(oldK K) error { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
_, exists := repo.data[oldK] | |
if !exists { | |
return errors.New("error: cannot remove, item is not present") | |
} | |
delete(repo.data, oldK) | |
return nil | |
} | |
func (repo *MemoryRepository[T, K]) Type() (t T) { | |
return t | |
} | |
func (repo *MemoryRepository[T, K]) KeyType() (k K) { | |
return k | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"errors" | |
"fmt" | |
"strings" | |
"sync" | |
"time" | |
) | |
type User struct { | |
ID int `json:"id"` | |
Name string `json:"name"` | |
Email string `json:"email"` | |
} | |
var users = []*User{ | |
{ID: 1, Name: "Scott", Email: "[email protected]"}, | |
{ID: 2, Name: "Matt", Email: "[email protected]"}, | |
{ID: 3, Name: "Dick", Email: "[email protected]"}, | |
{ID: 4, Name: "Frank", Email: "[email protected]"}, | |
{ID: 5, Name: "Jim", Email: "[email protected]"}, | |
} | |
func VerifyUserRepository(repo Repository[*User, int]) { | |
fmt.Printf("type=%T, keytype=%T\n", repo.Type(), repo.KeyType()) | |
} | |
func main() { | |
// create new memory repository | |
var err error | |
repo := NewMemoryRepository[*User, int]() | |
VerifyUserRepository(repo) | |
// insert six users | |
fmt.Printf("inserting 6 users:\n") | |
for i := 0; i < len(users); i++ { | |
err = repo.Insert(users[i].ID, users[i]) | |
if err != nil { | |
panic(err) | |
} | |
fmt.Printf("\tsuccessfully inserted: %+v\n", users[i]) | |
} | |
fmt.Println() | |
time.Sleep(1 * time.Second) | |
// find all users with an email address ending in example.com | |
var rs1 []*User | |
rs1, err = repo.Find(func(u *User) bool { return strings.HasSuffix(u.Email, "example.com") }) | |
if err != nil { | |
panic(err) | |
} | |
fmt.Printf("find all users with an email address ending in example.com\n") | |
fmt.Printf("\tfound %d results\n", len(rs1)) | |
for _, u := range rs1 { | |
fmt.Printf("\tsuccessfully found: %+v\n", u) | |
} | |
fmt.Println() | |
time.Sleep(1 * time.Second) | |
// find all users with an email address not ending in somewhere.com who have an odd ID | |
var rs2 []*User | |
rs2, err = repo.Find(func(u *User) bool { | |
return !strings.HasSuffix(u.Email, "somewhere.com") && u.ID%2 != 0 | |
}) | |
if err != nil { | |
panic(err) | |
} | |
fmt.Printf("find all users with an email address not ending in somewhere.com who have and odd ID\n") | |
fmt.Printf("\tfound %d results\n", len(rs2)) | |
for _, u := range rs2 { | |
fmt.Printf("\tsuccessfully found: %+v\n", u) | |
} | |
fmt.Println() | |
time.Sleep(1 * time.Second) | |
// delete the users who have a name that ends in "tt" | |
var n int | |
n, err = repo.FindAndExec( | |
func(u *User) bool { return strings.HasSuffix(u.Name, "tt") }, | |
func(u *User) bool { return repo.Delete(u.ID) == nil }) | |
if err != nil { | |
panic(err) | |
} | |
fmt.Printf("delete the users with a name ending in 'tt'\n") | |
fmt.Printf("\tsuccessfully completed %d operations\n", n) | |
fmt.Println() | |
time.Sleep(1 * time.Second) | |
// find all users | |
var rs3 []*User | |
rs3, err = repo.Find(func(u *User) bool { return u != nil }) | |
if err != nil { | |
panic(err) | |
} | |
fmt.Printf("find all users\n") | |
fmt.Printf("\tfound %d results\n", len(rs3)) | |
for _, u := range rs3 { | |
fmt.Printf("\tsuccessfully found: %+v\n", u) | |
} | |
fmt.Println() | |
time.Sleep(1 * time.Second) | |
} | |
// QueryFunc is a function that is injected with a type | |
// and returns a boolean | |
type QueryFunc[T any] func(t T) bool | |
// Repository is an interface for representing a generic | |
// data repository | |
type Repository[T any, K comparable] interface { | |
// Find provides the user with an interface for | |
// locating, retrieving or viewing one or more | |
// entries. | |
Find(query QueryFunc[T]) ([]T, error) | |
// FindAndExec provides the user with an interface for | |
// locating and executing a function on the items matching | |
// the query criteria. | |
FindAndExec(query QueryFunc[T], exec QueryFunc[T]) (int, error) | |
// Insert provides the user with an interface for | |
// creating and adding new entries. | |
Insert(newK K, newT T) error | |
// Update provides the user with an interface for | |
// editing or updating an existing entry. | |
Update(oldK K, newT T) error | |
// Delete provides the user with an interface for | |
// removing or invalidating an existing entry. | |
Delete(oldK K) error | |
// Type returns the data type that is used with the Repository | |
Type() T | |
// KeyType returns the data type that is used as a primary key with the Repository | |
KeyType() K | |
} | |
type MemoryRepository[T any, K comparable] struct { | |
lock sync.Mutex | |
data map[K]T | |
} | |
func NewMemoryRepository[T any, K comparable]() *MemoryRepository[T, K] { | |
return &MemoryRepository[T, K]{ | |
data: make(map[K]T), | |
} | |
} | |
func (repo *MemoryRepository[T, K]) Find(query QueryFunc[T]) ([]T, error) { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
if len(repo.data) < 1 { | |
return nil, errors.New("error: cannot find anything because there is no data") | |
} | |
var res []T | |
for k, t := range repo.data { | |
if query(t) { | |
res = append(res, repo.data[k]) | |
} | |
} | |
if len(res) == 0 { | |
return nil, errors.New("error: query did not match anything") | |
} | |
return res, nil | |
} | |
func (repo *MemoryRepository[T, K]) FindAndExec(query QueryFunc[T], exec QueryFunc[T]) (int, error) { | |
var res []T | |
f1 := func() (int, error) { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
if len(repo.data) < 1 { | |
return -1, errors.New("error: cannot find anything because there is no data") | |
} | |
for k, t := range repo.data { | |
if query(t) { | |
res = append(res, repo.data[k]) | |
} | |
} | |
if len(res) == 0 { | |
return 0, errors.New("error: query did not match anything") | |
} | |
return len(res), nil | |
} | |
f2 := func() (int, error) { | |
var ops int | |
for i := range res { | |
if exec(res[i]) { | |
ops++ | |
} | |
} | |
return ops, nil | |
} | |
n, err := f1() | |
if err != nil { | |
return n, err | |
} | |
return f2() | |
} | |
func (repo *MemoryRepository[T, K]) Insert(newK K, newT T) error { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
_, exists := repo.data[newK] | |
if exists { | |
return errors.New("error: cannot insert, item already exists") | |
} | |
repo.data[newK] = newT | |
return nil | |
} | |
func (repo *MemoryRepository[T, K]) Update(oldK K, newT T) error { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
_, exists := repo.data[oldK] | |
if !exists { | |
return errors.New("error: cannot update, item does not exist") | |
} | |
repo.data[oldK] = newT | |
return nil | |
} | |
func (repo *MemoryRepository[T, K]) Delete(oldK K) error { | |
repo.lock.Lock() | |
defer repo.lock.Unlock() | |
_, exists := repo.data[oldK] | |
if !exists { | |
return errors.New("error: cannot remove, item is not present") | |
} | |
delete(repo.data, oldK) | |
return nil | |
} | |
func (repo *MemoryRepository[T, K]) Type() (t T) { | |
return t | |
} | |
func (repo *MemoryRepository[T, K]) KeyType() (k K) { | |
return k | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment