mirror of https://github.com/VinGarcia/ksql.git
Add example tests to `examples/testing`
parent
7ab871dad2
commit
e49aa5f620
10
Makefile
10
Makefile
|
@ -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/...
|
||||
|
|
20
README.md
20
README.md
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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])
|
||||
})
|
||||
}
|
|
@ -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
4
go.mod
|
@ -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
20
go.sum
|
@ -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=
|
||||
|
|
240
kiss_orm.go
240
kiss_orm.go
|
@ -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) {
|
||||
|
|
139
kiss_orm_test.go
139
kiss_orm_test.go
|
@ -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,
|
||||
|
|
|
@ -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...)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue