Skip to content

Instantly share code, notes, and snippets.

@scottcagno
Last active January 20, 2024 22:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save scottcagno/dfe0a83bc2ca94ebf3817c5e62e8d27f to your computer and use it in GitHub Desktop.
Save scottcagno/dfe0a83bc2ca94ebf3817c5e62e8d27f to your computer and use it in GitHub Desktop.
Generic Repository idea and implementation
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
}
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: "scott@somewhere.com"},
{ID: 2, Name: "Matt", Email: "matt@example.com"},
{ID: 3, Name: "Dick", Email: "dick@example.com"},
{ID: 4, Name: "Frank", Email: "frank@foobar.com"},
{ID: 5, Name: "Jim", Email: "jim@foobar.com"},
}
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