mapper: moved mapper to separate file, added snake case mapper

Performance is comparable to sting.ToLower

BenchmarkSnakeCase-4              200000              5922 ns/op            1216 B/op         97 allocs/op
BenchmarkToLower-4                300000              5418 ns/op             704 B/op         74 allocs/op
This commit is contained in:
Michał Matczuk
2017-07-26 10:46:06 +02:00
parent 871a3693ad
commit 08b131c5ee
4 changed files with 131 additions and 12 deletions

View File

@@ -22,20 +22,22 @@ var placeSchema = `
CREATE TABLE gocqlx_test.place ( CREATE TABLE gocqlx_test.place (
country text, country text,
city text, city text,
telcode int, code int,
PRIMARY KEY(country, city) PRIMARY KEY(country, city)
)` )`
// Field names are converted to camel case by default, no need to add
// `db:"first_name"`, if you want to disable a filed add `db:"-"` tag
type Person struct { type Person struct {
FirstName string `db:"first_name"` FirstName string
LastName string `db:"last_name"` LastName string
Email []string Email []string
} }
type Place struct { type Place struct {
Country string Country string
City string City string
TelCode int TelCode int `db:"code"`
} }
func TestExample(t *testing.T) { func TestExample(t *testing.T) {
@@ -62,7 +64,7 @@ func TestExample(t *testing.T) {
{ {
mustExec(session.Query(placeSchema)) mustExec(session.Query(placeSchema))
q := session.Query("INSERT INTO gocqlx_test.place (country, city, telcode) VALUES (?, ?, ?)") q := session.Query("INSERT INTO gocqlx_test.place (country, city, code) VALUES (?, ?, ?)")
mustExec(q.Bind("United States", "New York", 1)) mustExec(q.Bind("United States", "New York", 1))
mustExec(q.Bind("Hong Kong", "", 852)) mustExec(q.Bind("Hong Kong", "", 852))
mustExec(q.Bind("Singapore", "", 65)) mustExec(q.Bind("Singapore", "", 65))

View File

@@ -4,18 +4,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"github.com/gocql/gocql" "github.com/gocql/gocql"
"github.com/jmoiron/sqlx/reflectx" "github.com/jmoiron/sqlx/reflectx"
) )
// DefaultMapper uses `db` tag and strings.ToLower to lowercase struct field
// names. It can be set to whatever you want, but it is encouraged to be set
// before gocqlx is used as name-to-field mappings are cached after first
// use on a type.
var DefaultMapper = reflectx.NewMapperFunc("db", strings.ToLower)
// structOnlyError returns an error appropriate for type when a non-scannable // structOnlyError returns an error appropriate for type when a non-scannable
// struct is expected but something else is given // struct is expected but something else is given
func structOnlyError(t reflect.Type) error { func structOnlyError(t reflect.Type) error {

40
mapper.go Normal file
View File

@@ -0,0 +1,40 @@
package gocqlx
import (
"fmt"
"unicode"
"github.com/jmoiron/sqlx/reflectx"
)
// DefaultMapper uses `db` tag and automatically converts struct field names to
// snake case. It can be set to whatever you want, but it is encouraged to be
// set before gocqlx is used as name-to-field mappings are cached after first
// use on a type.
var DefaultMapper = reflectx.NewMapperFunc("db", snakeCase)
// snakeCase converts camel case to snake case.
func snakeCase(s string) string {
buf := []byte(s)
out := make([]byte, 0, len(buf)+3)
l := len(buf)
for i := 0; i < l; i++ {
if !(allowedBindRune(buf[i]) || buf[i] == '_') {
panic(fmt.Sprint("not allowed name ", s))
}
b := rune(buf[i])
if unicode.IsUpper(b) {
if i > 0 && buf[i-1] != '_' && (unicode.IsLower(rune(buf[i-1])) || (i+1 < l && unicode.IsLower(rune(buf[i+1])))) {
out = append(out, '_')
}
b = unicode.ToLower(b)
}
out = append(out, byte(b))
}
return string(out)
}

84
mapper_test.go Normal file
View File

@@ -0,0 +1,84 @@
package gocqlx
import (
"strings"
"testing"
)
var snakeTests = []struct {
name string
expected string
}{
{"a", "a"},
{"snake", "snake"},
{"A", "a"},
{"ID", "id"},
{"MOTD", "motd"},
{"Snake", "snake"},
{"SnakeTest", "snake_test"},
{"APIResponse", "api_response"},
{"SnakeID", "snake_id"},
{"Snake_Id", "snake_id"},
{"Snake_ID", "snake_id"},
{"SnakeIDGoogle", "snake_id_google"},
{"LinuxMOTD", "linux_motd"},
{"OMGWTFBBQ", "omgwtfbbq"},
{"omg_wtf_bbq", "omg_wtf_bbq"},
{"woof_woof", "woof_woof"},
{"_woof_woof", "_woof_woof"},
{"woof_woof_", "woof_woof_"},
{"WOOF", "woof"},
{"Woof", "woof"},
{"woof", "woof"},
{"woof0_woof1", "woof0_woof1"},
{"_woof0_woof1_2", "_woof0_woof1_2"},
{"woof0_WOOF1_2", "woof0_woof1_2"},
{"WOOF0", "woof0"},
{"Woof1", "woof1"},
{"woof2", "woof2"},
{"woofWoof", "woof_woof"},
{"woofWOOF", "woof_woof"},
{"woof_WOOF", "woof_woof"},
{"Woof_WOOF", "woof_woof"},
{"WOOFWoofWoofWOOFWoofWoof", "woof_woof_woof_woof_woof_woof"},
{"WOOF_Woof_woof_WOOF_Woof_woof", "woof_woof_woof_woof_woof_woof"},
{"Woof_W", "woof_w"},
{"Woof_w", "woof_w"},
{"WoofW", "woof_w"},
{"Woof_W_", "woof_w_"},
{"Woof_w_", "woof_w_"},
{"WoofW_", "woof_w_"},
{"WOOF_", "woof_"},
{"W_Woof", "w_woof"},
{"w_Woof", "w_woof"},
{"WWoof", "w_woof"},
{"_W_Woof", "_w_woof"},
{"_w_Woof", "_w_woof"},
{"_WWoof", "_w_woof"},
{"_WOOF", "_woof"},
{"_woof", "_woof"},
}
func TestSnakeCase(t *testing.T) {
for _, tt := range snakeTests {
if actual := snakeCase(tt.name); actual != tt.expected {
t.Error("expected", tt.expected, "got", actual, tt)
}
}
}
func BenchmarkSnakeCase(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, test := range snakeTests {
snakeCase(test.name)
}
}
}
func BenchmarkToLower(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, test := range snakeTests {
strings.ToLower(test.name)
}
}
}