├── .gitignore ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── repository.go ├── repository_test.go ├── specification.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # IDEs 18 | .idea/ 19 | *.iml 20 | .vscode/ 21 | 22 | # Mac 23 | .DS_Store -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 9 | 10 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 11 | 12 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 13 | 14 | This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guideline 2 | Welcome to our project, the fact that you are finding yourself looking at the contributing guidelines for this project is great, we would love for you to contribute 3 | 4 | We want to make contributing as easy and transparent as possible, whether it's reporting a bug, submitting a fix, proposing new features or simply becoming a maintainer. 5 | 6 | ## Quick Links 7 | * [Getting Started](#getting-started) 8 | * [Our Branch Strategy](#our-branching-strategy) 9 | * [Raising Issues](#raising-issues) 10 | * [Code of Conduct](#code-of-conduct) 11 | 12 | ## Getting Started 13 | Contributions to this repo are made via Issues and Pull Requests (PRs). A few general guidelines that cover both: 14 | 15 | - Search for existing Issues and PRs before creating your own. 16 | - We work hard to makes sure issues are handled in a timely manner but, depending on the impact, it could take a while to investigate the root cause. A friendly ping in the comment thread to the submitter or a contributor can help draw attention if your issue is blocking. 17 | - If you've never contributed before, see issues that are labelled `good first issue` or `documentation` to slowly get familiar with the codebase 18 | - If you have any questions please ask! 19 | 20 | ### Our Branching Strategy 21 | 22 | We use [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow) as our branching strategy so all changes go through pull requests 23 | 24 | Pull request are the best way to propose changes to the codebase, and we actively welcome PRs. 25 | To submit a change: 26 | 1. Fork the repository and create a branch from `master` 27 | 2. Any code added should be tested, please add tests! 28 | 3. Any changes that impacts contracts (func parameters or APIs), please document them 29 | 4. All tests pass and code coverage is maintained or improved 30 | 5. Make sure you code adheres to the linting rules 31 | 6. Issue a pull request 32 | 1. PRs should always be linked to an issue on the repo 33 | 2. In the description please add as make explanation about the changes you have made 34 | 3. We have PR template to help you describe the changes been made 35 | 36 | ### Raising Issues 37 | We would love your feedback whether there is a bug in a version of the code, share potential enhancements or ask questions, please raise a [GitHub Issue](https://github.com/Ompluscator/gorm-generics/issues) 38 | 39 | When raising an issue please provide as much information as you possibly can in order for us to help you quicker. 40 | 41 | ## Code of Conduct 42 | We take our open source community seriously. We hold ourselves and other contributors to high standards of communication. 43 | By contributing to this project, you agree to uphold our [Code of Conduct](https://github.com/Ompluscator/gorm-generics/blob/master/CODE-OF-CONDUCT.md) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marko Milojevic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PoC for Go generics with GORM 2 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](code_of_conduct.md) 3 | 4 | ## Introduction 5 | This repository represents a small PoC for using 6 | [Go generics](https://levelup.gitconnected.com/generics-in-go-viva-la-revolution-e27898bf5495) together 7 | with [GORM](https://gorm.io/index.html), an Object-relational 8 | mapping library for Golang. 9 | 10 | At this stage it emphasizes possibilities, and it is not stable implementation. 11 | In this stage, it is not meant to be used for production system. 12 | 13 | Future development is the intention for this project, 14 | and any contribution is more than welcome. 15 | 16 | ## Example 17 | ```go 18 | package main 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "log" 24 | 25 | "github.com/ompluscator/gorm-generics" 26 | "gorm.io/driver/sqlite" 27 | "gorm.io/gorm" 28 | ) 29 | 30 | // Product is a domain entity 31 | type Product struct { 32 | ID uint 33 | Name string 34 | Weight uint 35 | IsAvailable bool 36 | } 37 | 38 | // ProductGorm is DTO used to map Product entity to database 39 | type ProductGorm struct { 40 | ID uint `gorm:"primaryKey;column:id"` 41 | Name string `gorm:"column:name"` 42 | Weight uint `gorm:"column:weight"` 43 | IsAvailable bool `gorm:"column:is_available"` 44 | } 45 | 46 | // ToEntity respects the gorm_generics.GormModel interface 47 | // Creates new Entity from GORM model. 48 | func (g ProductGorm) ToEntity() Product { 49 | return Product{ 50 | ID: g.ID, 51 | Name: g.Name, 52 | Weight: g.Weight, 53 | IsAvailable: g.IsAvailable, 54 | } 55 | } 56 | 57 | // FromEntity respects the gorm_generics.GormModel interface 58 | // Creates new GORM model from Entity. 59 | func (g ProductGorm) FromEntity(product Product) interface{} { 60 | return ProductGorm{ 61 | ID: product.ID, 62 | Name: product.Name, 63 | Weight: product.Weight, 64 | IsAvailable: product.IsAvailable, 65 | } 66 | } 67 | 68 | func main() { 69 | db, err := gorm.Open(sqlite.Open("file:test?mode=memory&cache=shared&_fk=1"), &gorm.Config{}) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | err = db.AutoMigrate(ProductGorm{}) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | 79 | repository := gorm_generics.NewRepository[ProductGorm, Product](db) 80 | 81 | ctx := context.Background() 82 | 83 | product := Product{ 84 | Name: "product1", 85 | Weight: 100, 86 | IsAvailable: true, 87 | } 88 | err = repository.Insert(ctx, &product) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | 93 | fmt.Println(product) 94 | // Out: 95 | // {1 product1 100 true} 96 | 97 | single, err := repository.FindByID(ctx, product.ID) 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | fmt.Println(single) 102 | // Out: 103 | // {1 product1 100 true} 104 | 105 | err = repository.Insert(ctx, &Product{ 106 | Name: "product2", 107 | Weight: 50, 108 | IsAvailable: true, 109 | }) 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | 114 | many, err := repository.Find(ctx, gorm_generics.GreaterOrEqual("weight", 50)) 115 | if err != nil { 116 | log.Fatal(err) 117 | } 118 | fmt.Println(many) 119 | // Out: 120 | // [{1 product1 100 true} {2 product2 50 true}] 121 | 122 | err = repository.Insert(ctx, &Product{ 123 | Name: "product3", 124 | Weight: 250, 125 | IsAvailable: false, 126 | }) 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | 131 | many, err = repository.Find(ctx, gorm_generics.GreaterOrEqual("weight", 90)) 132 | if err != nil { 133 | log.Fatal(err) 134 | } 135 | fmt.Println(many) 136 | // Out: 137 | // [{1 product1 100 true} {3 product3 250 false}] 138 | 139 | many, err = repository.Find(ctx, gorm_generics.And( 140 | gorm_generics.GreaterOrEqual("weight", 90), 141 | gorm_generics.Equal("is_available", true)), 142 | ) 143 | if err != nil { 144 | log.Fatal(err) 145 | } 146 | fmt.Println(many) 147 | // Out: 148 | // [{1 product1 100 true}] 149 | } 150 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ompluscator/gorm-generics 2 | 3 | go 1.18 4 | 5 | require ( 6 | gorm.io/driver/sqlite v1.4.3 7 | gorm.io/gorm v1.24.0 8 | ) 9 | 10 | require ( 11 | github.com/jinzhu/inflection v1.0.0 // indirect 12 | github.com/jinzhu/now v1.1.5 // indirect 13 | github.com/mattn/go-sqlite3 v1.14.15 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 2 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 3 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -------------------------------------------------------------------------------- /repository.go: -------------------------------------------------------------------------------- 1 | package gorm_generics 2 | 3 | import ( 4 | "context" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type GormModel[E any] interface { 9 | ToEntity() E 10 | FromEntity(entity E) interface{} 11 | } 12 | 13 | func NewRepository[M GormModel[E], E any](db *gorm.DB) *GormRepository[M, E] { 14 | return &GormRepository[M, E]{ 15 | db: db, 16 | } 17 | } 18 | 19 | type GormRepository[M GormModel[E], E any] struct { 20 | db *gorm.DB 21 | } 22 | 23 | func (r *GormRepository[M, E]) Insert(ctx context.Context, entity *E) error { 24 | var start M 25 | model := start.FromEntity(*entity).(M) 26 | 27 | err := r.db.WithContext(ctx).Create(&model).Error 28 | if err != nil { 29 | return err 30 | } 31 | 32 | *entity = model.ToEntity() 33 | return nil 34 | } 35 | 36 | func (r *GormRepository[M, E]) Delete(ctx context.Context, entity *E) error { 37 | var start M 38 | model := start.FromEntity(*entity).(M) 39 | err := r.db.WithContext(ctx).Delete(model).Error 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | 46 | func (r *GormRepository[M, E]) DeleteById(ctx context.Context, id any) error { 47 | var start M 48 | err := r.db.WithContext(ctx).Delete(&start, &id).Error 49 | if err != nil { 50 | return err 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func (r *GormRepository[M, E]) Update(ctx context.Context, entity *E) error { 57 | var start M 58 | model := start.FromEntity(*entity).(M) 59 | 60 | err := r.db.WithContext(ctx).Save(&model).Error 61 | if err != nil { 62 | return err 63 | } 64 | 65 | *entity = model.ToEntity() 66 | return nil 67 | } 68 | 69 | func (r *GormRepository[M, E]) FindByID(ctx context.Context, id any) (E, error) { 70 | var model M 71 | err := r.db.WithContext(ctx).First(&model, id).Error 72 | if err != nil { 73 | return *new(E), err 74 | } 75 | 76 | return model.ToEntity(), nil 77 | } 78 | 79 | func (r *GormRepository[M, E]) Find(ctx context.Context, specifications ...Specification) ([]E, error) { 80 | return r.FindWithLimit(ctx, -1, -1, specifications...) 81 | } 82 | 83 | func (r *GormRepository[M, E]) Count(ctx context.Context, specifications ...Specification) (i int64, err error) { 84 | model := new(M) 85 | err = r.getPreWarmDbForSelect(ctx, specifications...).Model(model).Count(&i).Error 86 | return 87 | } 88 | 89 | func (r *GormRepository[M, E]) getPreWarmDbForSelect(ctx context.Context, specification ...Specification) *gorm.DB { 90 | dbPrewarm := r.db.WithContext(ctx) 91 | for _, s := range specification { 92 | dbPrewarm = dbPrewarm.Where(s.GetQuery(), s.GetValues()...) 93 | } 94 | return dbPrewarm 95 | } 96 | func (r *GormRepository[M, E]) FindWithLimit(ctx context.Context, limit int, offset int, specifications ...Specification) ([]E, error) { 97 | var models []M 98 | 99 | dbPrewarm := r.getPreWarmDbForSelect(ctx, specifications...) 100 | err := dbPrewarm.Limit(limit).Offset(offset).Find(&models).Error 101 | 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | result := make([]E, 0, len(models)) 107 | for _, row := range models { 108 | result = append(result, row.ToEntity()) 109 | } 110 | 111 | return result, nil 112 | } 113 | 114 | func (r *GormRepository[M, E]) FindAll(ctx context.Context) ([]E, error) { 115 | return r.FindWithLimit(ctx, -1, -1) 116 | } 117 | -------------------------------------------------------------------------------- /repository_test.go: -------------------------------------------------------------------------------- 1 | package gorm_generics_test 2 | 3 | import ( 4 | "context" 5 | gorm_generics "github.com/ompluscator/gorm-generics" 6 | "gorm.io/driver/sqlite" 7 | "gorm.io/gorm" 8 | "gorm.io/gorm/logger" 9 | "log" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | // Product is a domain entity 15 | type Product struct { 16 | ID uint 17 | Name string 18 | Weight uint 19 | IsAvailable bool 20 | } 21 | 22 | // ProductGorm is DTO used to map Product entity to database 23 | type ProductGorm struct { 24 | ID uint `gorm:"primaryKey;column:id"` 25 | Name string `gorm:"column:name"` 26 | Weight uint `gorm:"column:weight"` 27 | IsAvailable bool `gorm:"column:is_available"` 28 | } 29 | 30 | // ToEntity respects the gorm_generics.GormModel interface 31 | // Creates new Entity from GORM model. 32 | func (g ProductGorm) ToEntity() Product { 33 | return Product{ 34 | ID: g.ID, 35 | Name: g.Name, 36 | Weight: g.Weight, 37 | IsAvailable: g.IsAvailable, 38 | } 39 | } 40 | 41 | // FromEntity respects the gorm_generics.GormModel interface 42 | // Creates new GORM model from Entity. 43 | func (g ProductGorm) FromEntity(product Product) interface{} { 44 | return ProductGorm{ 45 | ID: product.ID, 46 | Name: product.Name, 47 | Weight: product.Weight, 48 | IsAvailable: product.IsAvailable, 49 | } 50 | } 51 | 52 | func getDB() (*gorm.DB, error) { 53 | return gorm.Open(sqlite.Open("file:test?mode=memory&cache=shared&_fk=1"), &gorm.Config{ 54 | Logger: logger.Default.LogMode(logger.Silent), 55 | }) 56 | } 57 | func TestMain(m *testing.M) { 58 | db, err := getDB() 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | err = db.AutoMigrate(ProductGorm{}) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | ret := m.Run() 68 | os.Exit(ret) 69 | } 70 | func TestGormRepository_Insert(t *testing.T) { 71 | db, _ := getDB() 72 | repository := gorm_generics.NewRepository[ProductGorm, Product](db) 73 | ctx := context.Background() 74 | 75 | product := Product{ 76 | ID: 8, 77 | Name: "product1", 78 | Weight: 100, 79 | IsAvailable: true, 80 | } 81 | err := repository.Insert(ctx, &product) 82 | if err != nil { 83 | panic(err) 84 | } 85 | } 86 | 87 | func TestGormRepository_FindByID(t *testing.T) { 88 | db, _ := getDB() 89 | repository := gorm_generics.NewRepository[ProductGorm, Product](db) 90 | ctx := context.Background() 91 | 92 | _, err := repository.FindByID(ctx, 8) 93 | 94 | if err != nil { 95 | panic(err) 96 | } 97 | } 98 | 99 | func TestGormRepository_Count(t *testing.T) { 100 | db, _ := getDB() 101 | repository := gorm_generics.NewRepository[ProductGorm, Product](db) 102 | ctx := context.Background() 103 | 104 | nb, err := repository.Count(ctx) 105 | 106 | if err != nil { 107 | panic(err) 108 | } 109 | if nb != 1 { 110 | panic("not good count") 111 | } 112 | } 113 | 114 | func TestGormRepository_DeleteByID(t *testing.T) { 115 | db, _ := getDB() 116 | repository := gorm_generics.NewRepository[ProductGorm, Product](db) 117 | ctx := context.Background() 118 | err := repository.DeleteById(ctx, 8) 119 | if err != nil { 120 | panic(err) 121 | } 122 | _, err = repository.FindByID(ctx, 8) 123 | if err == nil { 124 | panic("supposed to be deleted") 125 | } 126 | } 127 | 128 | func TestGormRepository_Find(t *testing.T) { 129 | db, _ := getDB() 130 | repository := gorm_generics.NewRepository[ProductGorm, Product](db) 131 | ctx := context.Background() 132 | 133 | product := Product{ 134 | ID: 1, 135 | Name: "product1", 136 | Weight: 100, 137 | IsAvailable: true, 138 | } 139 | repository.Insert(ctx, &product) 140 | product2 := Product{ 141 | ID: 2, 142 | Name: "product2", 143 | Weight: 50, 144 | IsAvailable: true, 145 | } 146 | repository.Insert(ctx, &product2) 147 | many, err := repository.Find(ctx, gorm_generics.GreaterOrEqual("weight", 50)) 148 | if err != nil { 149 | panic(err) 150 | } 151 | if len(many) != 2 { 152 | panic("should be 2") 153 | } 154 | 155 | repository.Insert(ctx, &Product{ 156 | ID: 3, 157 | Name: "product3", 158 | Weight: 250, 159 | IsAvailable: false, 160 | }) 161 | 162 | many, err = repository.Find(ctx, gorm_generics.GreaterOrEqual("weight", 90)) 163 | if err != nil { 164 | panic(err) 165 | } 166 | if len(many) != 2 { 167 | panic("should be 2") 168 | } 169 | 170 | many, err = repository.Find(ctx, gorm_generics.And( 171 | gorm_generics.GreaterOrEqual("weight", 90), 172 | gorm_generics.Equal("is_available", true)), 173 | ) 174 | if err != nil { 175 | panic(err) 176 | } 177 | if len(many) != 1 { 178 | panic("should be 1") 179 | } 180 | } 181 | 182 | /* 183 | TODO 184 | Delete (by item) 185 | Update 186 | Find (with sql cond) 187 | */ 188 | -------------------------------------------------------------------------------- /specification.go: -------------------------------------------------------------------------------- 1 | package gorm_generics 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Specification interface { 9 | GetQuery() string 10 | GetValues() []any 11 | } 12 | 13 | type joinSpecification struct { 14 | specifications []Specification 15 | separator string 16 | } 17 | 18 | func (s joinSpecification) GetQuery() string { 19 | queries := make([]string, 0, len(s.specifications)) 20 | 21 | for _, spec := range s.specifications { 22 | queries = append(queries, spec.GetQuery()) 23 | } 24 | 25 | return strings.Join(queries, fmt.Sprintf(" %s ", s.separator)) 26 | } 27 | 28 | func (s joinSpecification) GetValues() []any { 29 | values := make([]any, 0) 30 | 31 | for _, spec := range s.specifications { 32 | values = append(values, spec.GetValues()...) 33 | } 34 | 35 | return values 36 | } 37 | 38 | func And(specifications ...Specification) Specification { 39 | return joinSpecification{ 40 | specifications: specifications, 41 | separator: "AND", 42 | } 43 | } 44 | 45 | func Or(specifications ...Specification) Specification { 46 | return joinSpecification{ 47 | specifications: specifications, 48 | separator: "OR", 49 | } 50 | } 51 | 52 | type notSpecification struct { 53 | Specification 54 | } 55 | 56 | func (s notSpecification) GetQuery() string { 57 | return fmt.Sprintf(" NOT (%s)", s.Specification.GetQuery()) 58 | } 59 | 60 | func Not(specification Specification) Specification { 61 | return notSpecification{ 62 | specification, 63 | } 64 | } 65 | 66 | type binaryOperatorSpecification[T any] struct { 67 | field string 68 | operator string 69 | value T 70 | } 71 | 72 | func (s binaryOperatorSpecification[T]) GetQuery() string { 73 | return fmt.Sprintf("%s %s ?", s.field, s.operator) 74 | } 75 | 76 | func (s binaryOperatorSpecification[T]) GetValues() []any { 77 | return []any{s.value} 78 | } 79 | 80 | func Equal[T any](field string, value T) Specification { 81 | return binaryOperatorSpecification[T]{ 82 | field: field, 83 | operator: "=", 84 | value: value, 85 | } 86 | } 87 | 88 | func GreaterThan[T comparable](field string, value T) Specification { 89 | return binaryOperatorSpecification[T]{ 90 | field: field, 91 | operator: ">", 92 | value: value, 93 | } 94 | } 95 | 96 | func GreaterOrEqual[T comparable](field string, value T) Specification { 97 | return binaryOperatorSpecification[T]{ 98 | field: field, 99 | operator: ">=", 100 | value: value, 101 | } 102 | } 103 | 104 | func LessThan[T comparable](field string, value T) Specification { 105 | return binaryOperatorSpecification[T]{ 106 | field: field, 107 | operator: "<", 108 | value: value, 109 | } 110 | } 111 | 112 | func LessOrEqual[T comparable](field string, value T) Specification { 113 | return binaryOperatorSpecification[T]{ 114 | field: field, 115 | operator: ">=", 116 | value: value, 117 | } 118 | } 119 | 120 | func In[T any](field string, value []T) Specification { 121 | return binaryOperatorSpecification[[]T]{ 122 | field: field, 123 | operator: "IN", 124 | value: value, 125 | } 126 | } 127 | 128 | type stringSpecification string 129 | 130 | func (s stringSpecification) GetQuery() string { 131 | return string(s) 132 | } 133 | 134 | func (s stringSpecification) GetValues() []any { 135 | return nil 136 | } 137 | 138 | func IsNull(field string) Specification { 139 | return stringSpecification(fmt.Sprintf("%s IS NULL", field)) 140 | } 141 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package gorm_generics 2 | 3 | func ChunkSlice[T any](slice []T, chunkSize int) [][]T { 4 | var chunks [][]T 5 | for i := 0; i < len(slice); i += chunkSize { 6 | end := i + chunkSize 7 | 8 | // necessary check to avoid slicing beyond 9 | // slice capacity 10 | if end > len(slice) { 11 | end = len(slice) 12 | } 13 | 14 | chunks = append(chunks, slice[i:end]) 15 | } 16 | 17 | return chunks 18 | } 19 | --------------------------------------------------------------------------------