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 (
country text,
city text,
telcode int,
code int,
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 {
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
FirstName string
LastName string
Email []string
}
type Place struct {
Country string
City string
TelCode int
TelCode int `db:"code"`
}
func TestExample(t *testing.T) {
@@ -62,7 +64,7 @@ func TestExample(t *testing.T) {
{
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("Hong Kong", "", 852))
mustExec(q.Bind("Singapore", "", 65))

View File

@@ -4,18 +4,11 @@ import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/gocql/gocql"
"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
// struct is expected but something else is given
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)
}
}
}