Add example tests to `examples/testing`

pull/2/head
Vinícius Garcia 2021-01-01 17:01:57 -03:00
parent 7ab871dad2
commit e49aa5f620
14 changed files with 1363 additions and 328 deletions

View File

@ -11,8 +11,18 @@ lint: setup
@go vet $(path) $(args) @go vet $(path) $(args)
@echo "Golint & Go Vet found no problems on your code!" @echo "Golint & Go Vet found no problems on your code!"
mock: setup
mockgen -package=exampleservice -source=contracts.go -destination=examples/testing/mocks.go
setup: .make.setup setup: .make.setup
.make.setup: .make.setup:
go get github.com/kyoh86/richgo go get github.com/kyoh86/richgo
go get golang.org/x/lint go get golang.org/x/lint
@# (Gomock is used on examples/testing)
go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen
touch .make.setup touch .make.setup
# Running examples:
exampleservice:
$(GOPATH)/bin/richgo test ./examples/testing/...

View File

@ -12,6 +12,13 @@ The goals were:
- It should be easy to mock and test (very easy) - It should be easy to mock and test (very easy)
- It should be above all readable. - It should be above all readable.
**Supported Drivers:**
Currently we only support 2 Drivers:
- `"postgres"`
- `"sqlite3"`
### Usage examples ### Usage examples
This example is also available [here][./examples/crud/crud.go] This example is also available [here][./examples/crud/crud.go]
@ -43,7 +50,7 @@ type PartialUpdateUser struct {
func main() { func main() {
ctx := context.Background() ctx := context.Background()
db, err := kissorm.NewClient("sqlite3", "/tmp/hello.sqlite", 1, "users") db, err := kissorm.New("sqlite3", "/tmp/hello.sqlite", 1, "users")
if err != nil { if err != nil {
panic(err.Error()) panic(err.Error())
} }
@ -149,3 +156,14 @@ type ORMProvider interface {
``` ```
You might notice we are lacking an abstraction for transactions, but it is on our TODO list. You might notice we are lacking an abstraction for transactions, but it is on our TODO list.
### TODO List
- Add support for transactions
- Improve error messages
- Allow the ID field to have a different name
- Allow database replicas for reading
- Fix a bug that is causing "database locked" errors when some the tests fail
- Implement a method of saving and struct fields as JSON on the database (an retrieving them)
- Double check if all reflection is safe on the Insert() function
- Make sure SELECT * works even if not all fields are present

109
examples/crud/crud.go Normal file
View File

@ -0,0 +1,109 @@
package main
import (
"context"
"fmt"
_ "github.com/mattn/go-sqlite3"
"github.com/vingarcia/kissorm"
"github.com/vingarcia/kissorm/nullable"
)
type User struct {
ID int `kissorm:"id"`
Name string `kissorm:"name"`
Age int `kissorm:"age"`
}
type PartialUpdateUser struct {
ID int `kissorm:"id"`
Name *string `kissorm:"name"`
Age *int `kissorm:"age"`
}
func main() {
ctx := context.Background()
db, err := kissorm.New("sqlite3", "/tmp/hello.sqlite", 1, "users")
if err != nil {
panic(err.Error())
}
err = db.Exec(ctx, `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
age INTEGER,
name TEXT
)`)
if err != nil {
panic(err.Error())
}
var alison = User{
Name: "Alison",
Age: 22,
}
err = db.Insert(ctx, &alison)
if err != nil {
panic(err.Error())
}
fmt.Println("Alison ID:", alison.ID)
// Inserting inline:
err = db.Insert(ctx, &User{
Name: "Cristina",
Age: 27,
})
if err != nil {
panic(err.Error())
}
// Deleting Alison:
err = db.Delete(ctx, alison.ID)
if err != nil {
panic(err.Error())
}
// Retrieving Cristina:
var cris User
err = db.QueryOne(ctx, &cris, "SELECT * FROM users WHERE name = ? ORDER BY id", "Cristina")
if err != nil {
panic(err.Error())
}
fmt.Printf("Cristina: %#v\n", cris)
// Updating all fields from Cristina:
cris.Name = "Cris"
err = db.Update(ctx, cris)
// Changing the age of Cristina but not touching any other fields:
// Partial update technique 1:
err = db.Update(ctx, struct {
ID int `kissorm:"id"`
Age int `kissorm:"age"`
}{ID: cris.ID, Age: 28})
if err != nil {
panic(err.Error())
}
// Partial update technique 2:
err = db.Update(ctx, PartialUpdateUser{
ID: cris.ID,
Age: nullable.Int(28),
})
if err != nil {
panic(err.Error())
}
// Listing first 10 users from the database
// (each time you run this example a new Cristina is created)
//
// Note: Using this function it is recommended to set a LIMIT, since
// not doing so can load too many users on your computer's memory or
// cause an Out Of Memory Kill.
var users []User
err = db.Query(ctx, &users, "SELECT * FROM users LIMIT 10")
if err != nil {
panic(err.Error())
}
fmt.Printf("Users: %#v\n", users)
}

View File

@ -0,0 +1,105 @@
package exampleservice
import (
"context"
"time"
"github.com/vingarcia/kissorm"
"github.com/vingarcia/kissorm/nullable"
)
// Service ...
type Service struct {
usersTable kissorm.ORMProvider
streamChunkSize int
}
// UserEntity represents a domain user,
// the pointer fields represent optional fields that
// might not be present in some requests.
//
// Its recommended that this struct contains
// one field for each database column,
// so you can write generic queries like `SELECT * FROM users`.
//
// If this is not the case, it might be a good idea
// to create a DTO struct to receive select queries.
type UserEntity struct {
ID int `kissorm:"id"`
Name *string `kissorm:"name"`
Age *int `kissorm:"age"`
Score *int `kissorm:"score"`
LastPayment time.Time `kissorm:"last_payment"`
}
// NewUserService ...
func NewUserService(usersTable kissorm.ORMProvider) Service {
return Service{
usersTable: usersTable,
streamChunkSize: 100,
}
}
// CreateUser ...
func (s Service) CreateUser(ctx context.Context, u UserEntity) error {
return s.usersTable.Insert(ctx, &u)
}
// UpdateUserScore update the user score adding scoreChange with the current
// user score. Defaults to 0 if not set.
func (s Service) UpdateUserScore(ctx context.Context, uID int, scoreChange int) error {
var scoreRow struct {
Score int `kissorm:"score"`
}
err := s.usersTable.QueryOne(ctx, &scoreRow, "SELECT score FROM users WHERE id = ?", uID)
if err != nil {
return err
}
return s.usersTable.Update(ctx, &UserEntity{
ID: uID,
Score: nullable.Int(scoreRow.Score + scoreChange),
})
}
// ListUsers returns a page of users
func (s Service) ListUsers(ctx context.Context, offset, limit int) (total int, users []UserEntity, err error) {
var countRow struct {
Count int `kissorm:"count"`
}
err = s.usersTable.QueryOne(ctx, &countRow, "SELECT count(*) as count FROM users")
if err != nil {
return 0, nil, err
}
return countRow.Count, users, s.usersTable.Query(ctx, &users, "SELECT * FROM users OFFSET ? LIMIT ?", offset, limit)
}
// StreamAllUsers sends all users from the database to an external client
//
// Note: This method is unusual, but so are the use-cases for the QueryChunks function.
// In most cases you should just use the Query or QueryOne functions and use the QueryChunks
// function only when the ammount of data loaded might exceed the available memory and/or
// when you can't put an upper limit on the number of values returned.
func (s Service) StreamAllUsers(ctx context.Context, sendUser func(u UserEntity) error) error {
return s.usersTable.QueryChunks(ctx, kissorm.ChunkParser{
Query: "SELECT * FROM users",
Params: []interface{}{},
ChunkSize: s.streamChunkSize,
ForEachChunk: func(users []UserEntity) error {
for _, user := range users {
err := sendUser(user)
if err != nil {
// This will abort the QueryChunks loop and return this error
return err
}
}
return nil
},
})
}
// DeleteUser deletes a user by its ID
func (s Service) DeleteUser(ctx context.Context, uID int) error {
return s.usersTable.Delete(ctx, uID)
}

View File

@ -0,0 +1,284 @@
package exampleservice
import (
"context"
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"github.com/tj/assert"
"github.com/vingarcia/kissorm"
"github.com/vingarcia/kissorm/nullable"
)
func TestCreateUser(t *testing.T) {
t.Run("should call kissorm.Insert correctly", func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
usersTableMock := NewMockORMProvider(controller)
s := Service{
usersTable: usersTableMock,
streamChunkSize: 100,
}
var users []interface{}
usersTableMock.EXPECT().Insert(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, records ...interface{}) error {
users = append(users, records...)
return nil
})
user := UserEntity{Name: nullable.String("TestUser")}
err := s.CreateUser(context.TODO(), user)
assert.Nil(t, err)
assert.Equal(t, 1, len(users))
assert.Equal(t, &user, users[0])
})
t.Run("another way of testing input structs", func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
usersTableMock := NewMockORMProvider(controller)
s := Service{
usersTable: usersTableMock,
streamChunkSize: 100,
}
var users []map[string]interface{}
usersTableMock.EXPECT().Insert(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, records ...interface{}) error {
for _, record := range records {
// The StructToMap function will convert a struct with `kissorm` tags
// into a map using the kissorm attr names as keys.
//
// If you are inserting an anonymous struct (not usual) this function
// can make your tests shorter:
uMap, err := kissorm.StructToMap(record)
if err != nil {
return err
}
users = append(users, uMap)
}
return nil
})
user := UserEntity{Name: nullable.String("TestUser")}
err := s.CreateUser(context.TODO(), user)
assert.Nil(t, err)
assert.Equal(t, 1, len(users))
assert.Equal(t, "TestUser", users[0]["name"])
})
}
func TestUpdateUserScore(t *testing.T) {
t.Run("should call kissorm.QueryOne() & Update() correctly", func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
usersTableMock := NewMockORMProvider(controller)
s := Service{
usersTable: usersTableMock,
streamChunkSize: 100,
}
var users []interface{}
gomock.InOrder(
usersTableMock.EXPECT().QueryOne(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, result interface{}, query string, params ...interface{}) error {
// This function will use reflection to fill the
// struct fields with the values from the map
return kissorm.FillStructWith(result, map[string]interface{}{
// Use int this map the keys you set on the kissorm tags, e.g. `kissorm:"score"`
// Each of these fields represent the database rows returned
// by the query.
"score": 42,
})
}),
usersTableMock.EXPECT().Update(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, records ...interface{}) error {
users = append(users, records...)
return nil
}),
)
err := s.UpdateUserScore(context.TODO(), 1, -2)
assert.Nil(t, err)
assert.Equal(t, 1, len(users))
resultUser := UserEntity{
ID: 1,
Score: nullable.Int(40),
}
assert.Equal(t, &resultUser, users[0])
})
}
func TestListUsers(t *testing.T) {
t.Run("should call kissorm.QueryOne() & Query() correctly", func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
usersTableMock := NewMockORMProvider(controller)
s := Service{
usersTable: usersTableMock,
streamChunkSize: 100,
}
gomock.InOrder(
usersTableMock.EXPECT().QueryOne(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, result interface{}, query string, params ...interface{}) error {
// This function will use reflection to fill the
// struct fields with the values from the map
return kissorm.FillStructWith(result, map[string]interface{}{
// Use int this map the keys you set on the kissorm tags, e.g. `kissorm:"score"`
// Each of these fields represent the database rows returned
// by the query.
"count": 420,
})
}),
usersTableMock.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, results interface{}, query string, params ...interface{}) error {
return kissorm.FillSliceWith(results, []map[string]interface{}{
{
"id": 1,
"name": "fake name",
"age": 42,
},
{
"id": 2,
"name": "another fake name",
"age": 43,
},
})
}),
)
total, users, err := s.ListUsers(context.TODO(), 40, 2)
assert.Nil(t, err)
assert.Equal(t, 420, total)
assert.Equal(t, 2, len(users))
expectedUsers := []UserEntity{
{
ID: 1,
Name: nullable.String("fake name"),
Age: nullable.Int(42),
},
{
ID: 2,
Name: nullable.String("another fake name"),
Age: nullable.Int(43),
},
}
assert.Equal(t, expectedUsers, users)
})
}
func TestStreamAllUsers(t *testing.T) {
t.Run("should call kissorm.QueryChunks correctly", func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
usersTableMock := NewMockORMProvider(controller)
s := Service{
usersTable: usersTableMock,
streamChunkSize: 2,
}
usersTableMock.EXPECT().QueryChunks(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, parser kissorm.ChunkParser) error {
fn, ok := parser.ForEachChunk.(func(users []UserEntity) error)
require.True(t, ok)
// Chunk 1:
err := fn([]UserEntity{
{
ID: 1,
Name: nullable.String("fake name"),
Age: nullable.Int(42),
},
{
ID: 2,
Name: nullable.String("another fake name"),
Age: nullable.Int(43),
},
})
if err != nil {
return err
}
// Chunk 2:
err = fn([]UserEntity{
{
ID: 3,
Name: nullable.String("yet another fake name"),
Age: nullable.Int(44),
},
})
return err
})
var users []UserEntity
err := s.StreamAllUsers(context.TODO(), func(u UserEntity) error {
users = append(users, u)
return nil
})
assert.Nil(t, err)
assert.Equal(t, 3, len(users))
expectedUsers := []UserEntity{
{
ID: 1,
Name: nullable.String("fake name"),
Age: nullable.Int(42),
},
{
ID: 2,
Name: nullable.String("another fake name"),
Age: nullable.Int(43),
},
{
ID: 3,
Name: nullable.String("yet another fake name"),
Age: nullable.Int(44),
},
}
assert.Equal(t, expectedUsers, users)
})
}
func TestDeleteUser(t *testing.T) {
t.Run("should call kissorm.Delete correctly", func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
usersTableMock := NewMockORMProvider(controller)
s := Service{
usersTable: usersTableMock,
streamChunkSize: 100,
}
var ids []interface{}
usersTableMock.EXPECT().Delete(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, idArgs ...interface{}) error {
ids = append(ids, idArgs...)
return nil
})
err := s.DeleteUser(context.TODO(), 42)
assert.Nil(t, err)
assert.Equal(t, 1, len(ids))
assert.Equal(t, 42, ids[0])
})
}

164
examples/testing/mocks.go Normal file
View File

@ -0,0 +1,164 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: contracts.go
// Package exampleservice is a generated GoMock package.
package exampleservice
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
kissorm "github.com/vingarcia/kissorm"
)
// MockORMProvider is a mock of ORMProvider interface.
type MockORMProvider struct {
ctrl *gomock.Controller
recorder *MockORMProviderMockRecorder
}
// MockORMProviderMockRecorder is the mock recorder for MockORMProvider.
type MockORMProviderMockRecorder struct {
mock *MockORMProvider
}
// NewMockORMProvider creates a new mock instance.
func NewMockORMProvider(ctrl *gomock.Controller) *MockORMProvider {
mock := &MockORMProvider{ctrl: ctrl}
mock.recorder = &MockORMProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockORMProvider) EXPECT() *MockORMProviderMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockORMProvider) Delete(ctx context.Context, ids ...interface{}) error {
m.ctrl.T.Helper()
varargs := []interface{}{ctx}
for _, a := range ids {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Delete", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockORMProviderMockRecorder) Delete(ctx interface{}, ids ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx}, ids...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockORMProvider)(nil).Delete), varargs...)
}
// Exec mocks base method.
func (m *MockORMProvider) Exec(ctx context.Context, query string, params ...interface{}) error {
m.ctrl.T.Helper()
varargs := []interface{}{ctx, query}
for _, a := range params {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockORMProviderMockRecorder) Exec(ctx, query interface{}, params ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx, query}, params...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockORMProvider)(nil).Exec), varargs...)
}
// Insert mocks base method.
func (m *MockORMProvider) Insert(ctx context.Context, records ...interface{}) error {
m.ctrl.T.Helper()
varargs := []interface{}{ctx}
for _, a := range records {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Insert", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert.
func (mr *MockORMProviderMockRecorder) Insert(ctx interface{}, records ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx}, records...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockORMProvider)(nil).Insert), varargs...)
}
// Query mocks base method.
func (m *MockORMProvider) Query(ctx context.Context, records interface{}, query string, params ...interface{}) error {
m.ctrl.T.Helper()
varargs := []interface{}{ctx, records, query}
for _, a := range params {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockORMProviderMockRecorder) Query(ctx, records, query interface{}, params ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx, records, query}, params...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockORMProvider)(nil).Query), varargs...)
}
// QueryChunks mocks base method.
func (m *MockORMProvider) QueryChunks(ctx context.Context, parser kissorm.ChunkParser) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryChunks", ctx, parser)
ret0, _ := ret[0].(error)
return ret0
}
// QueryChunks indicates an expected call of QueryChunks.
func (mr *MockORMProviderMockRecorder) QueryChunks(ctx, parser interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryChunks", reflect.TypeOf((*MockORMProvider)(nil).QueryChunks), ctx, parser)
}
// QueryOne mocks base method.
func (m *MockORMProvider) QueryOne(ctx context.Context, record interface{}, query string, params ...interface{}) error {
m.ctrl.T.Helper()
varargs := []interface{}{ctx, record, query}
for _, a := range params {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryOne", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// QueryOne indicates an expected call of QueryOne.
func (mr *MockORMProviderMockRecorder) QueryOne(ctx, record, query interface{}, params ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx, record, query}, params...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOne", reflect.TypeOf((*MockORMProvider)(nil).QueryOne), varargs...)
}
// Update mocks base method.
func (m *MockORMProvider) Update(ctx context.Context, records ...interface{}) error {
m.ctrl.T.Helper()
varargs := []interface{}{ctx}
for _, a := range records {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Update", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update.
func (mr *MockORMProviderMockRecorder) Update(ctx interface{}, records ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx}, records...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockORMProvider)(nil).Update), varargs...)
}

4
go.mod
View File

@ -4,6 +4,10 @@ go 1.14
require ( require (
github.com/ditointernet/go-assert v0.0.0-20200120164340-9e13125a7018 github.com/ditointernet/go-assert v0.0.0-20200120164340-9e13125a7018
github.com/golang/mock v1.4.4
github.com/lib/pq v1.1.1 github.com/lib/pq v1.1.1
github.com/mattn/go-sqlite3 v1.14.6 github.com/mattn/go-sqlite3 v1.14.6
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.6.1
github.com/tj/assert v0.0.3
) )

20
go.sum
View File

@ -1,14 +1,34 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ditointernet/go-assert v0.0.0-20200120164340-9e13125a7018 h1:QsFkVafcKOaZoAB4WcyUHdkPbwh+VYwZgYJb/rU6EIM= github.com/ditointernet/go-assert v0.0.0-20200120164340-9e13125a7018 h1:QsFkVafcKOaZoAB4WcyUHdkPbwh+VYwZgYJb/rU6EIM=
github.com/ditointernet/go-assert v0.0.0-20200120164340-9e13125a7018/go.mod h1:5C3SWkut69TSdkerzRDxXMRM5x73PGWNcRLe/xKjXhs= github.com/ditointernet/go-assert v0.0.0-20200120164340-9e13125a7018/go.mod h1:5C3SWkut69TSdkerzRDxXMRM5x73PGWNcRLe/xKjXhs=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -8,37 +8,37 @@ import (
"strings" "strings"
) )
// Client ... // DB ...
type Client struct { type DB struct {
driver string driver string
dialect dialect dialect dialect
tableName string tableName string
db *sql.DB db *sql.DB
} }
// NewClient instantiates a new client // New instantiates a new client
func NewClient( func New(
dbDriver string, dbDriver string,
connectionString string, connectionString string,
maxOpenConns int, maxOpenConns int,
tableName string, tableName string,
) (Client, error) { ) (DB, error) {
db, err := sql.Open(dbDriver, connectionString) db, err := sql.Open(dbDriver, connectionString)
if err != nil { if err != nil {
return Client{}, err return DB{}, err
} }
if err = db.Ping(); err != nil { if err = db.Ping(); err != nil {
return Client{}, err return DB{}, err
} }
db.SetMaxOpenConns(maxOpenConns) db.SetMaxOpenConns(maxOpenConns)
dialect := getDriverDialect(dbDriver) dialect := getDriverDialect(dbDriver)
if dialect == nil { if dialect == nil {
return Client{}, fmt.Errorf("unsupported driver `%s`", dbDriver) return DB{}, fmt.Errorf("unsupported driver `%s`", dbDriver)
} }
return Client{ return DB{
dialect: dialect, dialect: dialect,
driver: dbDriver, driver: dbDriver,
db: db, db: db,
@ -47,8 +47,8 @@ func NewClient(
} }
// ChangeTable creates a new client configured to query on a different table // ChangeTable creates a new client configured to query on a different table
func (c Client) ChangeTable(ctx context.Context, tableName string) ORMProvider { func (c DB) ChangeTable(ctx context.Context, tableName string) ORMProvider {
return &Client{ return &DB{
db: c.db, db: c.db,
tableName: tableName, tableName: tableName,
} }
@ -61,7 +61,7 @@ func (c Client) ChangeTable(ctx context.Context, tableName string) ORMProvider {
// Note: it is very important to make sure the query will // Note: it is very important to make sure the query will
// return a small known number of results, otherwise you risk // return a small known number of results, otherwise you risk
// of overloading the available memory. // of overloading the available memory.
func (c Client) Query( func (c DB) Query(
ctx context.Context, ctx context.Context,
records interface{}, records interface{},
query string, query string,
@ -130,7 +130,7 @@ func (c Client) Query(
// //
// QueryOne returns a ErrRecordNotFound if // QueryOne returns a ErrRecordNotFound if
// the query returns no results. // the query returns no results.
func (c Client) QueryOne( func (c DB) QueryOne(
ctx context.Context, ctx context.Context,
record interface{}, record interface{},
query string, query string,
@ -182,7 +182,7 @@ func (c Client) QueryOne(
// pointers to struct as its only argument and that reflection // pointers to struct as its only argument and that reflection
// will be used to instantiate this argument and to fill it // will be used to instantiate this argument and to fill it
// with the database rows. // with the database rows.
func (c Client) QueryChunks( func (c DB) QueryChunks(
ctx context.Context, ctx context.Context,
parser ChunkParser, parser ChunkParser,
) error { ) error {
@ -268,7 +268,7 @@ func (c Client) QueryChunks(
// //
// If the original instances have been passed by reference // If the original instances have been passed by reference
// the ID is automatically updated after insertion is completed. // the ID is automatically updated after insertion is completed.
func (c Client) Insert( func (c DB) Insert(
ctx context.Context, ctx context.Context,
records ...interface{}, records ...interface{},
) error { ) error {
@ -295,7 +295,7 @@ func (c Client) Insert(
return nil return nil
} }
func (c Client) insertOnPostgres( func (c DB) insertOnPostgres(
ctx context.Context, ctx context.Context,
record interface{}, record interface{},
query string, query string,
@ -331,7 +331,7 @@ func (c Client) insertOnPostgres(
return rows.Close() return rows.Close()
} }
func (c Client) insertWithLastInsertID( func (c DB) insertWithLastInsertID(
ctx context.Context, ctx context.Context,
record interface{}, record interface{},
query string, query string,
@ -357,7 +357,7 @@ func (c Client) insertWithLastInsertID(
} }
// Delete deletes one or more instances from the database by id // Delete deletes one or more instances from the database by id
func (c Client) Delete( func (c DB) Delete(
ctx context.Context, ctx context.Context,
ids ...interface{}, ids ...interface{},
) error { ) error {
@ -375,7 +375,7 @@ func (c Client) Delete(
// Update updates the given instances on the database by id. // Update updates the given instances on the database by id.
// //
// Partial updates are supported, i.e. it will ignore nil pointer attributes // Partial updates are supported, i.e. it will ignore nil pointer attributes
func (c Client) Update( func (c DB) Update(
ctx context.Context, ctx context.Context,
records ...interface{}, records ...interface{},
) error { ) error {
@ -492,208 +492,18 @@ func buildUpdateQuery(
return query, args, nil return query, args, nil
} }
// Exec just runs an SQL command on the database returning no rows.
func (c DB) Exec(ctx context.Context, query string, params ...interface{}) error {
_, err := c.db.ExecContext(ctx, query, params...)
return err
}
// This cache is kept as a pkg variable // This cache is kept as a pkg variable
// because the total number of types on a program // because the total number of types on a program
// should be finite. So keeping a single cache here // should be finite. So keeping a single cache here
// works fine. // works fine.
var tagInfoCache = map[reflect.Type]structInfo{} var tagInfoCache = map[reflect.Type]structInfo{}
type structInfo struct {
Names map[int]string
Index map[string]int
}
// StructToMap converts any struct type to a map based on
// the tag named `kissorm`, i.e. `kissorm:"map_key_name"`
//
// Valid pointers are dereferenced and copied to the map,
// null pointers are ignored.
//
// This function is efficient in the fact that it caches
// the slower steps of the reflection required to perform
// this task.
func StructToMap(obj interface{}) (map[string]interface{}, error) {
v := reflect.ValueOf(obj)
t := v.Type()
if t.Kind() == reflect.Ptr {
v = v.Elem()
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("input must be a struct or struct pointer")
}
info := getCachedTagInfo(tagInfoCache, t)
m := map[string]interface{}{}
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
ft := field.Type()
if ft.Kind() == reflect.Ptr {
if field.IsNil() {
continue
}
field = field.Elem()
}
m[info.Names[i]] = field.Interface()
}
return m, nil
}
// This function collects only the names
// that will be used from the input type.
//
// This should save several calls to `Field(i).Tag.Get("foo")`
// which improves performance by a lot.
func getTagNames(t reflect.Type) structInfo {
info := structInfo{
Names: map[int]string{},
Index: map[string]int{},
}
for i := 0; i < t.NumField(); i++ {
name := t.Field(i).Tag.Get("kissorm")
if name == "" {
continue
}
info.Names[i] = name
info.Index[name] = i
}
return info
}
// FillStructWith is meant to be used on unit tests to mock
// the response from the database.
//
// The first argument is any struct you are passing to a kissorm func,
// and the second is a map representing a database row you want
// to use to update this struct.
func FillStructWith(record interface{}, dbRow map[string]interface{}) error {
v := reflect.ValueOf(record)
t := v.Type()
if t.Kind() != reflect.Ptr {
return fmt.Errorf(
"FillStructWith: expected input to be a pointer to struct but got %T",
record,
)
}
t = t.Elem()
v = v.Elem()
if t.Kind() != reflect.Struct {
return fmt.Errorf(
"FillStructWith: expected input kind to be a struct but got %T",
record,
)
}
info := getCachedTagInfo(tagInfoCache, t)
for colName, attr := range dbRow {
attrValue := reflect.ValueOf(attr)
field := v.Field(info.Index[colName])
fieldType := t.Field(info.Index[colName]).Type
if !attrValue.Type().ConvertibleTo(fieldType) {
return fmt.Errorf(
"FillStructWith: cannot convert atribute %s of type %v to type %T",
colName,
fieldType,
record,
)
}
field.Set(attrValue.Convert(fieldType))
}
return nil
}
// FillSliceWith is meant to be used on unit tests to mock
// the response from the database.
//
// The first argument is any slice of structs you are passing to a kissorm func,
// and the second is a slice of maps representing the database rows you want
// to use to update this struct.
func FillSliceWith(entities interface{}, dbRows []map[string]interface{}) error {
sliceRef := reflect.ValueOf(entities)
sliceType := sliceRef.Type()
if sliceType.Kind() != reflect.Ptr {
return fmt.Errorf(
"FillSliceWith: expected input to be a pointer to struct but got %v",
sliceType,
)
}
structType, isSliceOfPtrs, err := decodeAsSliceOfStructs(sliceType.Elem())
if err != nil {
return fmt.Errorf("FillSliceWith: %s", err.Error())
}
slice := sliceRef.Elem()
for idx, row := range dbRows {
if slice.Len() <= idx {
var elemValue reflect.Value
elemValue = reflect.New(structType)
if !isSliceOfPtrs {
elemValue = elemValue.Elem()
}
slice = reflect.Append(slice, elemValue)
}
err := FillStructWith(slice.Index(idx).Addr().Interface(), row)
if err != nil {
return err
}
}
sliceRef.Elem().Set(slice)
return nil
}
// Exec just runs an SQL command on the database returning no rows.
func (c Client) Exec(ctx context.Context, query string, params ...interface{}) error {
_, err := c.db.ExecContext(ctx, query, params...)
return err
}
func decodeAsSliceOfStructs(slice reflect.Type) (
structType reflect.Type,
isSliceOfPtrs bool,
err error,
) {
if slice.Kind() != reflect.Slice {
err = fmt.Errorf(
"expected input kind to be a slice but got %v",
slice,
)
return
}
elemType := slice.Elem()
isPtr := elemType.Kind() == reflect.Ptr
if isPtr {
elemType = elemType.Elem()
}
if elemType.Kind() != reflect.Struct {
err = fmt.Errorf(
"expected input to be a slice of structs but got %v",
slice,
)
return
}
return elemType, isPtr, nil
}
var errType = reflect.TypeOf(new(error)).Elem() var errType = reflect.TypeOf(new(error)).Elem()
func parseInputFunc(fn interface{}) (reflect.Type, error) { func parseInputFunc(fn interface{}) (reflect.Type, error) {

View File

@ -32,7 +32,7 @@ func TestQuery(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
var users []User var users []User
err := c.Query(ctx, &users, `SELECT * FROM users WHERE id=1;`) err := c.Query(ctx, &users, `SELECT * FROM users WHERE id=1;`)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
@ -52,7 +52,7 @@ func TestQuery(t *testing.T) {
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
var users []User var users []User
err = c.Query(ctx, &users, `SELECT * FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia") err = c.Query(ctx, &users, `SELECT * FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia")
@ -73,7 +73,7 @@ func TestQuery(t *testing.T) {
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
var users []User var users []User
err = c.Query(ctx, &users, `SELECT * FROM users WHERE name like `+c.dialect.Placeholder(0), "% Garcia") err = c.Query(ctx, &users, `SELECT * FROM users WHERE name like `+c.dialect.Placeholder(0), "% Garcia")
@ -96,7 +96,7 @@ func TestQuery(t *testing.T) {
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, "postgres", "users") c := newTestDB(db, "postgres", "users")
err = c.Query(ctx, &User{}, `SELECT * FROM users WHERE name like `+c.dialect.Placeholder(0), "% Sá") err = c.Query(ctx, &User{}, `SELECT * FROM users WHERE name like `+c.dialect.Placeholder(0), "% Sá")
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
@ -127,7 +127,7 @@ func TestQueryOne(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, "postgres", "users") c := newTestDB(db, "postgres", "users")
u := User{} u := User{}
err := c.QueryOne(ctx, &u, `SELECT * FROM users WHERE id=1;`) err := c.QueryOne(ctx, &u, `SELECT * FROM users WHERE id=1;`)
assert.Equal(t, ErrRecordNotFound, err) assert.Equal(t, ErrRecordNotFound, err)
@ -141,7 +141,7 @@ func TestQueryOne(t *testing.T) {
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, "postgres", "users") c := newTestDB(db, "postgres", "users")
u := User{} u := User{}
err = c.QueryOne(ctx, &u, `SELECT * FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia") err = c.QueryOne(ctx, &u, `SELECT * FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia")
@ -161,7 +161,7 @@ func TestQueryOne(t *testing.T) {
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, "postgres", "users") c := newTestDB(db, "postgres", "users")
err = c.QueryOne(ctx, &[]User{}, `SELECT * FROM users WHERE name like `+c.dialect.Placeholder(0), "% Sá") err = c.QueryOne(ctx, &[]User{}, `SELECT * FROM users WHERE name like `+c.dialect.Placeholder(0), "% Sá")
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
@ -186,7 +186,7 @@ func TestInsert(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
err = c.Insert(ctx) err = c.Insert(ctx)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
@ -197,7 +197,7 @@ func TestInsert(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
u := User{ u := User{
Name: "Fernanda", Name: "Fernanda",
@ -230,7 +230,7 @@ func TestDelete(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
u := User{ u := User{
Name: "Won't be deleted", Name: "Won't be deleted",
@ -260,7 +260,7 @@ func TestDelete(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
u1 := User{ u1 := User{
Name: "Fernanda", Name: "Fernanda",
@ -308,7 +308,7 @@ func TestDelete(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
u1 := User{ u1 := User{
Name: "Fernanda", Name: "Fernanda",
@ -373,7 +373,7 @@ func TestUpdate(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
u := User{ u := User{
Name: "Thay", Name: "Thay",
@ -398,7 +398,7 @@ func TestUpdate(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
u := User{ u := User{
Name: "Letícia", Name: "Letícia",
@ -429,7 +429,7 @@ func TestUpdate(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
u := User{ u := User{
Name: "Letícia", Name: "Letícia",
@ -460,7 +460,7 @@ func TestUpdate(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
type partialUser struct { type partialUser struct {
ID uint `kissorm:"id"` ID uint `kissorm:"id"`
@ -501,7 +501,7 @@ func TestUpdate(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
type partialUser struct { type partialUser struct {
ID uint `kissorm:"id"` ID uint `kissorm:"id"`
@ -542,7 +542,7 @@ func TestUpdate(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "non_existing_table") c := newTestDB(db, driver, "non_existing_table")
err = c.Update(ctx, User{ err = c.Update(ctx, User{
ID: 1, ID: 1,
@ -554,68 +554,6 @@ func TestUpdate(t *testing.T) {
} }
} }
func TestStructToMap(t *testing.T) {
type S1 struct {
Name string `kissorm:"name_attr"`
Age int `kissorm:"age_attr"`
}
t.Run("should convert plain structs to maps", func(t *testing.T) {
m, err := StructToMap(S1{
Name: "my name",
Age: 22,
})
assert.Equal(t, nil, err)
assert.Equal(t, map[string]interface{}{
"name_attr": "my name",
"age_attr": 22,
}, m)
})
t.Run("should not ignore zero value attrs, if they are not pointers", func(t *testing.T) {
m, err := StructToMap(S1{
Name: "",
Age: 0,
})
assert.Equal(t, nil, err)
assert.Equal(t, map[string]interface{}{
"name_attr": "",
"age_attr": 0,
}, m)
})
type S2 struct {
Name *string `kissorm:"name"`
Age *int `kissorm:"age"`
}
t.Run("should not ignore not nil pointers", func(t *testing.T) {
str := ""
age := 0
m, err := StructToMap(S2{
Name: &str,
Age: &age,
})
assert.Equal(t, nil, err)
assert.Equal(t, map[string]interface{}{
"name": "",
"age": 0,
}, m)
})
t.Run("should ignore nil pointers", func(t *testing.T) {
m, err := StructToMap(S2{
Name: nil,
Age: nil,
})
assert.Equal(t, nil, err)
assert.Equal(t, map[string]interface{}{}, m)
})
}
func TestQueryChunks(t *testing.T) { func TestQueryChunks(t *testing.T) {
for _, driver := range []string{"sqlite3", "postgres"} { for _, driver := range []string{"sqlite3", "postgres"} {
t.Run(driver, func(t *testing.T) { t.Run(driver, func(t *testing.T) {
@ -629,7 +567,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"}) _ = c.Insert(ctx, &User{Name: "User1"})
@ -665,7 +603,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"}) _ = c.Insert(ctx, &User{Name: "User1"})
_ = c.Insert(ctx, &User{Name: "User2"}) _ = c.Insert(ctx, &User{Name: "User2"})
@ -703,7 +641,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"}) _ = c.Insert(ctx, &User{Name: "User1"})
_ = c.Insert(ctx, &User{Name: "User2"}) _ = c.Insert(ctx, &User{Name: "User2"})
@ -741,7 +679,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"}) _ = c.Insert(ctx, &User{Name: "User1"})
_ = c.Insert(ctx, &User{Name: "User2"}) _ = c.Insert(ctx, &User{Name: "User2"})
@ -782,7 +720,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"}) _ = c.Insert(ctx, &User{Name: "User1"})
_ = c.Insert(ctx, &User{Name: "User2"}) _ = c.Insert(ctx, &User{Name: "User2"})
@ -821,7 +759,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
c := newTestClient(db, driver, "users") c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"}) _ = c.Insert(ctx, &User{Name: "User1"})
_ = c.Insert(ctx, &User{Name: "User2"}) _ = c.Insert(ctx, &User{Name: "User2"})
@ -857,29 +795,6 @@ func TestQueryChunks(t *testing.T) {
} }
} }
func TestFillSliceWith(t *testing.T) {
t.Run("should fill a list correctly", func(t *testing.T) {
var users []User
err := FillSliceWith(&users, []map[string]interface{}{
{
"name": "Jorge",
},
{
"name": "Luciana",
},
{
"name": "Breno",
},
})
assert.Equal(t, nil, err)
assert.Equal(t, 3, len(users))
assert.Equal(t, "Jorge", users[0].Name)
assert.Equal(t, "Luciana", users[1].Name)
assert.Equal(t, "Breno", users[2].Name)
})
}
func TestScanRows(t *testing.T) { func TestScanRows(t *testing.T) {
t.Run("should scan users correctly", func(t *testing.T) { t.Run("should scan users correctly", func(t *testing.T) {
err := createTable("sqlite3") err := createTable("sqlite3")
@ -890,7 +805,7 @@ func TestScanRows(t *testing.T) {
ctx := context.TODO() ctx := context.TODO()
db := connectDB(t, "sqlite3") db := connectDB(t, "sqlite3")
defer db.Close() defer db.Close()
c := newTestClient(db, "sqlite3", "users") c := newTestDB(db, "sqlite3", "users")
_ = c.Insert(ctx, &User{Name: "User1", Age: 22}) _ = c.Insert(ctx, &User{Name: "User1", Age: 22})
_ = c.Insert(ctx, &User{Name: "User2", Age: 14}) _ = c.Insert(ctx, &User{Name: "User2", Age: 14})
_ = c.Insert(ctx, &User{Name: "User3", Age: 43}) _ = c.Insert(ctx, &User{Name: "User3", Age: 43})
@ -1010,8 +925,8 @@ func createTable(driver string) error {
return nil return nil
} }
func newTestClient(db *sql.DB, driver string, tableName string) Client { func newTestDB(db *sql.DB, driver string, tableName string) DB {
return Client{ return DB{
driver: driver, driver: driver,
dialect: getDriverDialect(driver), dialect: getDriverDialect(driver),
db: db, db: db,

43
mocks.go Normal file
View File

@ -0,0 +1,43 @@
package kissorm
import "context"
type MockORMProvider struct {
InsertFn func(ctx context.Context, records ...interface{}) error
DeleteFn func(ctx context.Context, ids ...interface{}) error
UpdateFn func(ctx context.Context, records ...interface{}) error
QueryFn func(ctx context.Context, records interface{}, query string, params ...interface{}) error
QueryOneFn func(ctx context.Context, record interface{}, query string, params ...interface{}) error
QueryChunksFn func(ctx context.Context, parser ChunkParser) error
ExecFn func(ctx context.Context, query string, params ...interface{}) error
}
func (m MockORMProvider) Insert(ctx context.Context, records ...interface{}) error {
return m.InsertFn(ctx, records...)
}
func (m MockORMProvider) Delete(ctx context.Context, ids ...interface{}) error {
return m.DeleteFn(ctx, ids...)
}
func (m MockORMProvider) Update(ctx context.Context, records ...interface{}) error {
return m.UpdateFn(ctx, records...)
}
func (m MockORMProvider) Query(ctx context.Context, records interface{}, query string, params ...interface{}) error {
return m.QueryFn(ctx, records, query, params...)
}
func (m MockORMProvider) QueryOne(ctx context.Context, record interface{}, query string, params ...interface{}) error {
return m.QueryOneFn(ctx, record, query, params...)
}
func (m MockORMProvider) QueryChunks(ctx context.Context, parser ChunkParser) error {
return m.QueryChunksFn(ctx, parser)
}
func (m MockORMProvider) Exec(ctx context.Context, query string, params ...interface{}) error {
return m.ExecFn(ctx, query, params...)
}

View File

@ -5,6 +5,56 @@ func Int(i int) *int {
return &i return &i
} }
// Int8 ...
func Int8(i int8) *int8 {
return &i
}
// Int16 ...
func Int16(i int16) *int16 {
return &i
}
// Int32 ...
func Int32(i int32) *int32 {
return &i
}
// Int64 ...
func Int64(i int64) *int64 {
return &i
}
// UInt ...
func UInt(i int) *int {
return &i
}
// UInt8 ...
func UInt8(i int8) *int8 {
return &i
}
// UInt16 ...
func UInt16(i int16) *int16 {
return &i
}
// UInt32 ...
func UInt32(i int32) *int32 {
return &i
}
// UInt64 ...
func UInt64(i int64) *int64 {
return &i
}
// Float32 ...
func Float32(f float32) *float32 {
return &f
}
// Float64 ... // Float64 ...
func Float64(f float64) *float64 { func Float64(f float64) *float64 {
return &f return &f
@ -14,3 +64,28 @@ func Float64(f float64) *float64 {
func String(s string) *string { func String(s string) *string {
return &s return &s
} }
// Bool ...
func Bool(b bool) *bool {
return &b
}
// Rune ...
func Rune(r rune) *rune {
return &r
}
// Byte ...
func Byte(b byte) *byte {
return &b
}
// Complex64 ...
func Complex64(c complex64) *complex64 {
return &c
}
// Complex128 ...
func Complex128(c complex128) *complex128 {
return &c
}

267
structs.go Normal file
View File

@ -0,0 +1,267 @@
package kissorm
import (
"fmt"
"reflect"
"github.com/pkg/errors"
)
type structInfo struct {
Names map[int]string
Index map[string]int
}
// StructToMap converts any struct type to a map based on
// the tag named `kissorm`, i.e. `kissorm:"map_key_name"`
//
// Valid pointers are dereferenced and copied to the map,
// null pointers are ignored.
//
// This function is efficient in the fact that it caches
// the slower steps of the reflection required to perform
// this task.
func StructToMap(obj interface{}) (map[string]interface{}, error) {
v := reflect.ValueOf(obj)
t := v.Type()
if t.Kind() == reflect.Ptr {
v = v.Elem()
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("input must be a struct or struct pointer")
}
info := getCachedTagInfo(tagInfoCache, t)
m := map[string]interface{}{}
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
ft := field.Type()
if ft.Kind() == reflect.Ptr {
if field.IsNil() {
continue
}
field = field.Elem()
}
m[info.Names[i]] = field.Interface()
}
return m, nil
}
// FillStructWith is meant to be used on unit tests to mock
// the response from the database.
//
// The first argument is any struct you are passing to a kissorm func,
// and the second is a map representing a database row you want
// to use to update this struct.
func FillStructWith(record interface{}, dbRow map[string]interface{}) error {
v := reflect.ValueOf(record)
t := v.Type()
if t.Kind() != reflect.Ptr {
return fmt.Errorf(
"FillStructWith: expected input to be a pointer to struct but got %T",
record,
)
}
t = t.Elem()
v = v.Elem()
if t.Kind() != reflect.Struct {
return fmt.Errorf(
"FillStructWith: expected input kind to be a struct but got %T",
record,
)
}
info := getCachedTagInfo(tagInfoCache, t)
for colName, rawSrc := range dbRow {
src := NewPtrConverter(rawSrc)
dest := v.Field(info.Index[colName])
destType := t.Field(info.Index[colName]).Type
destValue, err := src.Convert(destType)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("FillStructWith: error on field `%s`", colName))
}
dest.Set(destValue)
}
return nil
}
// This type was created to make it easier
// to handle conversion between ptr and non ptr types, e.g.:
//
// - *type to *type
// - type to *type
// - *type to type
// - type to type
type PtrConverter struct {
BaseType reflect.Type
BaseValue reflect.Value
ElemType reflect.Type
ElemValue reflect.Value
}
func NewPtrConverter(v interface{}) PtrConverter {
if v == nil {
// This is necessary so that reflect.ValueOf
// returns a valid reflect.Value
v = (*interface{})(nil)
}
baseValue := reflect.ValueOf(v)
baseType := reflect.TypeOf(v)
elemType := baseType
elemValue := baseValue
if baseType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
elemValue = elemValue.Elem()
}
return PtrConverter{
BaseType: baseType,
BaseValue: baseValue,
ElemType: elemType,
ElemValue: elemValue,
}
}
func (p PtrConverter) Convert(destType reflect.Type) (reflect.Value, error) {
destElemType := destType
if destType.Kind() == reflect.Ptr {
destElemType = destType.Elem()
}
// Return 0 valued destType instance:
if p.BaseType.Kind() == reflect.Ptr && p.BaseValue.IsNil() {
// Note that if destType is a ptr it will return a nil ptr.
return reflect.New(destType).Elem(), nil
}
if !p.ElemType.ConvertibleTo(destElemType) {
return reflect.Value{}, fmt.Errorf(
"cannot convert from type %v to type %v", p.BaseType, destType,
)
}
destValue := p.ElemValue.Convert(destElemType)
// Get the address of destValue if necessary:
if destType.Kind() == reflect.Ptr {
if !destValue.CanAddr() {
tmp := reflect.New(destElemType)
tmp.Elem().Set(destValue)
destValue = tmp
} else {
destValue = destValue.Addr()
}
}
return destValue, nil
}
// FillSliceWith is meant to be used on unit tests to mock
// the response from the database.
//
// The first argument is any slice of structs you are passing to a kissorm func,
// and the second is a slice of maps representing the database rows you want
// to use to update this struct.
func FillSliceWith(entities interface{}, dbRows []map[string]interface{}) error {
sliceRef := reflect.ValueOf(entities)
sliceType := sliceRef.Type()
if sliceType.Kind() != reflect.Ptr {
return fmt.Errorf(
"FillSliceWith: expected input to be a pointer to struct but got %v",
sliceType,
)
}
structType, isSliceOfPtrs, err := decodeAsSliceOfStructs(sliceType.Elem())
if err != nil {
return errors.Wrap(err, "FillSliceWith")
}
slice := sliceRef.Elem()
for idx, row := range dbRows {
if slice.Len() <= idx {
var elemValue reflect.Value
elemValue = reflect.New(structType)
if !isSliceOfPtrs {
elemValue = elemValue.Elem()
}
slice = reflect.Append(slice, elemValue)
}
err := FillStructWith(slice.Index(idx).Addr().Interface(), row)
if err != nil {
return errors.Wrap(err, "FillSliceWith")
}
}
sliceRef.Elem().Set(slice)
return nil
}
// This function collects only the names
// that will be used from the input type.
//
// This should save several calls to `Field(i).Tag.Get("foo")`
// which improves performance by a lot.
func getTagNames(t reflect.Type) structInfo {
info := structInfo{
Names: map[int]string{},
Index: map[string]int{},
}
for i := 0; i < t.NumField(); i++ {
name := t.Field(i).Tag.Get("kissorm")
if name == "" {
continue
}
info.Names[i] = name
info.Index[name] = i
}
return info
}
func decodeAsSliceOfStructs(slice reflect.Type) (
structType reflect.Type,
isSliceOfPtrs bool,
err error,
) {
if slice.Kind() != reflect.Slice {
err = fmt.Errorf(
"expected input kind to be a slice but got %v",
slice,
)
return
}
elemType := slice.Elem()
isPtr := elemType.Kind() == reflect.Ptr
if isPtr {
elemType = elemType.Elem()
}
if elemType.Kind() != reflect.Struct {
err = fmt.Errorf(
"expected input to be a slice of structs but got %v",
slice,
)
return
}
return elemType, isPtr, nil
}

211
structs_test.go Normal file
View File

@ -0,0 +1,211 @@
package kissorm
import (
"testing"
"github.com/ditointernet/go-assert"
"github.com/vingarcia/kissorm/nullable"
)
func TestStructToMap(t *testing.T) {
type S1 struct {
Name string `kissorm:"name_attr"`
Age int `kissorm:"age_attr"`
}
t.Run("should convert plain structs to maps", func(t *testing.T) {
m, err := StructToMap(S1{
Name: "my name",
Age: 22,
})
assert.Equal(t, nil, err)
assert.Equal(t, map[string]interface{}{
"name_attr": "my name",
"age_attr": 22,
}, m)
})
t.Run("should not ignore zero value attrs, if they are not pointers", func(t *testing.T) {
m, err := StructToMap(S1{
Name: "",
Age: 0,
})
assert.Equal(t, nil, err)
assert.Equal(t, map[string]interface{}{
"name_attr": "",
"age_attr": 0,
}, m)
})
type S2 struct {
Name *string `kissorm:"name"`
Age *int `kissorm:"age"`
}
t.Run("should not ignore not nil pointers", func(t *testing.T) {
str := ""
age := 0
m, err := StructToMap(S2{
Name: &str,
Age: &age,
})
assert.Equal(t, nil, err)
assert.Equal(t, map[string]interface{}{
"name": "",
"age": 0,
}, m)
})
t.Run("should ignore nil pointers", func(t *testing.T) {
m, err := StructToMap(S2{
Name: nil,
Age: nil,
})
assert.Equal(t, nil, err)
assert.Equal(t, map[string]interface{}{}, m)
})
}
func TestFillStructWith(t *testing.T) {
t.Run("should fill a struct correctly", func(t *testing.T) {
var user struct {
Name string `kissorm:"name"`
Age int `kissorm:"age"`
}
err := FillStructWith(&user, map[string]interface{}{
"name": "Breno",
"age": 22,
})
assert.Equal(t, nil, err)
assert.Equal(t, "Breno", user.Name)
assert.Equal(t, 22, user.Age)
})
t.Run("should fill ptr fields with ptr values", func(t *testing.T) {
var user struct {
Name *string `kissorm:"name"`
Age *int `kissorm:"age"`
}
err := FillStructWith(&user, map[string]interface{}{
"name": nullable.String("Breno"),
"age": nullable.Int(22),
})
assert.Equal(t, nil, err)
assert.Equal(t, nullable.String("Breno"), user.Name)
assert.Equal(t, nullable.Int(22), user.Age)
})
t.Run("should fill ptr fields with non-ptr values", func(t *testing.T) {
var user struct {
Name *string `kissorm:"name"`
Age *int `kissorm:"age"`
}
err := FillStructWith(&user, map[string]interface{}{
"name": "Breno",
"age": 22,
})
assert.Equal(t, nil, err)
assert.Equal(t, nullable.String("Breno"), user.Name)
assert.Equal(t, nullable.Int(22), user.Age)
})
t.Run("should fill non ptr fields with ptr values", func(t *testing.T) {
var user struct {
Name string `kissorm:"name"`
Age int `kissorm:"age"`
}
err := FillStructWith(&user, map[string]interface{}{
"name": nullable.String("Breno"),
"age": nullable.Int(22),
})
assert.Equal(t, nil, err)
assert.Equal(t, "Breno", user.Name)
assert.Equal(t, 22, user.Age)
})
t.Run("should fill ptr fields with nil when necessary", func(t *testing.T) {
var user struct {
Name *string `kissorm:"name"`
Age *int `kissorm:"age"`
}
err := FillStructWith(&user, map[string]interface{}{
"name": nil,
"age": nil,
})
assert.Equal(t, nil, err)
assert.Equal(t, (*string)(nil), user.Name)
assert.Equal(t, (*int)(nil), user.Age)
})
t.Run("should interpret nil fields as zero values when necessary", func(t *testing.T) {
var user struct {
Name string `kissorm:"name"`
Age int `kissorm:"age"`
}
user.Name = "not empty"
user.Age = 42
err := FillStructWith(&user, map[string]interface{}{
"name": nil,
"age": nil,
})
assert.Equal(t, nil, err)
assert.Equal(t, "", user.Name)
assert.Equal(t, 0, user.Age)
})
t.Run("should ignore extra or missing fields", func(t *testing.T) {
var user struct {
Name string `kissorm:"name"`
Age int `kissorm:"age"`
Missing string `kissorm:"missing"`
}
user.Missing = "should be untouched"
err := FillStructWith(&user, map[string]interface{}{
"name": "fake name",
"age": 42,
"extra_field": "some value",
})
assert.Equal(t, nil, err)
assert.Equal(t, "fake name", user.Name)
assert.Equal(t, 42, user.Age)
assert.Equal(t, "should be untouched", user.Missing)
})
}
func TestFillSliceWith(t *testing.T) {
t.Run("should fill a list correctly", func(t *testing.T) {
var users []struct {
Name string `kissorm:"name"`
Age int `kissorm:"age"`
}
err := FillSliceWith(&users, []map[string]interface{}{
{
"name": "Jorge",
},
{
"name": "Luciana",
},
{
"name": "Breno",
},
})
assert.Equal(t, nil, err)
assert.Equal(t, 3, len(users))
assert.Equal(t, "Jorge", users[0].Name)
assert.Equal(t, "Luciana", users[1].Name)
assert.Equal(t, "Breno", users[2].Name)
})
}