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)
@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
.make.setup:
go get github.com/kyoh86/richgo
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
# 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 above all readable.
**Supported Drivers:**
Currently we only support 2 Drivers:
- `"postgres"`
- `"sqlite3"`
### Usage examples
This example is also available [here][./examples/crud/crud.go]
@ -43,7 +50,7 @@ type PartialUpdateUser struct {
func main() {
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 {
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.
### 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 (
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/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/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/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/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/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"
)
// Client ...
type Client struct {
// DB ...
type DB struct {
driver string
dialect dialect
tableName string
db *sql.DB
}
// NewClient instantiates a new client
func NewClient(
// New instantiates a new client
func New(
dbDriver string,
connectionString string,
maxOpenConns int,
tableName string,
) (Client, error) {
) (DB, error) {
db, err := sql.Open(dbDriver, connectionString)
if err != nil {
return Client{}, err
return DB{}, err
}
if err = db.Ping(); err != nil {
return Client{}, err
return DB{}, err
}
db.SetMaxOpenConns(maxOpenConns)
dialect := getDriverDialect(dbDriver)
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,
driver: dbDriver,
db: db,
@ -47,8 +47,8 @@ func NewClient(
}
// ChangeTable creates a new client configured to query on a different table
func (c Client) ChangeTable(ctx context.Context, tableName string) ORMProvider {
return &Client{
func (c DB) ChangeTable(ctx context.Context, tableName string) ORMProvider {
return &DB{
db: c.db,
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
// return a small known number of results, otherwise you risk
// of overloading the available memory.
func (c Client) Query(
func (c DB) Query(
ctx context.Context,
records interface{},
query string,
@ -130,7 +130,7 @@ func (c Client) Query(
//
// QueryOne returns a ErrRecordNotFound if
// the query returns no results.
func (c Client) QueryOne(
func (c DB) QueryOne(
ctx context.Context,
record interface{},
query string,
@ -182,7 +182,7 @@ func (c Client) QueryOne(
// pointers to struct as its only argument and that reflection
// will be used to instantiate this argument and to fill it
// with the database rows.
func (c Client) QueryChunks(
func (c DB) QueryChunks(
ctx context.Context,
parser ChunkParser,
) error {
@ -268,7 +268,7 @@ func (c Client) QueryChunks(
//
// If the original instances have been passed by reference
// the ID is automatically updated after insertion is completed.
func (c Client) Insert(
func (c DB) Insert(
ctx context.Context,
records ...interface{},
) error {
@ -295,7 +295,7 @@ func (c Client) Insert(
return nil
}
func (c Client) insertOnPostgres(
func (c DB) insertOnPostgres(
ctx context.Context,
record interface{},
query string,
@ -331,7 +331,7 @@ func (c Client) insertOnPostgres(
return rows.Close()
}
func (c Client) insertWithLastInsertID(
func (c DB) insertWithLastInsertID(
ctx context.Context,
record interface{},
query string,
@ -357,7 +357,7 @@ func (c Client) insertWithLastInsertID(
}
// Delete deletes one or more instances from the database by id
func (c Client) Delete(
func (c DB) Delete(
ctx context.Context,
ids ...interface{},
) error {
@ -375,7 +375,7 @@ func (c Client) Delete(
// Update updates the given instances on the database by id.
//
// Partial updates are supported, i.e. it will ignore nil pointer attributes
func (c Client) Update(
func (c DB) Update(
ctx context.Context,
records ...interface{},
) error {
@ -492,208 +492,18 @@ func buildUpdateQuery(
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
// because the total number of types on a program
// should be finite. So keeping a single cache here
// works fine.
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()
func parseInputFunc(fn interface{}) (reflect.Type, error) {

View File

@ -32,7 +32,7 @@ func TestQuery(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
var users []User
err := c.Query(ctx, &users, `SELECT * FROM users WHERE id=1;`)
assert.Equal(t, nil, err)
@ -52,7 +52,7 @@ func TestQuery(t *testing.T) {
assert.Equal(t, nil, err)
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
var users []User
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)
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
var users []User
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)
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á")
assert.NotEqual(t, nil, err)
@ -127,7 +127,7 @@ func TestQueryOne(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, "postgres", "users")
c := newTestDB(db, "postgres", "users")
u := User{}
err := c.QueryOne(ctx, &u, `SELECT * FROM users WHERE id=1;`)
assert.Equal(t, ErrRecordNotFound, err)
@ -141,7 +141,7 @@ func TestQueryOne(t *testing.T) {
assert.Equal(t, nil, err)
ctx := context.Background()
c := newTestClient(db, "postgres", "users")
c := newTestDB(db, "postgres", "users")
u := User{}
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)
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á")
assert.NotEqual(t, nil, err)
@ -186,7 +186,7 @@ func TestInsert(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
err = c.Insert(ctx)
assert.Equal(t, nil, err)
@ -197,7 +197,7 @@ func TestInsert(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
u := User{
Name: "Fernanda",
@ -230,7 +230,7 @@ func TestDelete(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
u := User{
Name: "Won't be deleted",
@ -260,7 +260,7 @@ func TestDelete(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
u1 := User{
Name: "Fernanda",
@ -308,7 +308,7 @@ func TestDelete(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
u1 := User{
Name: "Fernanda",
@ -373,7 +373,7 @@ func TestUpdate(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
u := User{
Name: "Thay",
@ -398,7 +398,7 @@ func TestUpdate(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
u := User{
Name: "Letícia",
@ -429,7 +429,7 @@ func TestUpdate(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
u := User{
Name: "Letícia",
@ -460,7 +460,7 @@ func TestUpdate(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
type partialUser struct {
ID uint `kissorm:"id"`
@ -501,7 +501,7 @@ func TestUpdate(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
type partialUser struct {
ID uint `kissorm:"id"`
@ -542,7 +542,7 @@ func TestUpdate(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "non_existing_table")
c := newTestDB(db, driver, "non_existing_table")
err = c.Update(ctx, User{
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) {
for _, driver := range []string{"sqlite3", "postgres"} {
t.Run(driver, func(t *testing.T) {
@ -629,7 +567,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"})
@ -665,7 +603,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"})
_ = c.Insert(ctx, &User{Name: "User2"})
@ -703,7 +641,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"})
_ = c.Insert(ctx, &User{Name: "User2"})
@ -741,7 +679,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"})
_ = c.Insert(ctx, &User{Name: "User2"})
@ -782,7 +720,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"})
_ = c.Insert(ctx, &User{Name: "User2"})
@ -821,7 +759,7 @@ func TestQueryChunks(t *testing.T) {
defer db.Close()
ctx := context.Background()
c := newTestClient(db, driver, "users")
c := newTestDB(db, driver, "users")
_ = c.Insert(ctx, &User{Name: "User1"})
_ = 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) {
t.Run("should scan users correctly", func(t *testing.T) {
err := createTable("sqlite3")
@ -890,7 +805,7 @@ func TestScanRows(t *testing.T) {
ctx := context.TODO()
db := connectDB(t, "sqlite3")
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: "User2", Age: 14})
_ = c.Insert(ctx, &User{Name: "User3", Age: 43})
@ -1010,8 +925,8 @@ func createTable(driver string) error {
return nil
}
func newTestClient(db *sql.DB, driver string, tableName string) Client {
return Client{
func newTestDB(db *sql.DB, driver string, tableName string) DB {
return DB{
driver: driver,
dialect: getDriverDialect(driver),
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
}
// 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 ...
func Float64(f float64) *float64 {
return &f
@ -14,3 +64,28 @@ func Float64(f float64) *float64 {
func String(s string) *string {
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)
})
}