From 08b131c5eee745e7eefbd2f0b849b35864c07e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Matczuk?= Date: Wed, 26 Jul 2017 10:46:06 +0200 Subject: [PATCH] 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 --- example_test.go | 12 ++++--- gocqlx.go | 7 ----- mapper.go | 40 +++++++++++++++++++++++ mapper_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 mapper.go create mode 100644 mapper_test.go diff --git a/example_test.go b/example_test.go index b8738e5..403b2c6 100644 --- a/example_test.go +++ b/example_test.go @@ -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)) diff --git a/gocqlx.go b/gocqlx.go index 518df9d..0fc541a 100644 --- a/gocqlx.go +++ b/gocqlx.go @@ -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 { diff --git a/mapper.go b/mapper.go new file mode 100644 index 0000000..fb3859d --- /dev/null +++ b/mapper.go @@ -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) +} diff --git a/mapper_test.go b/mapper_test.go new file mode 100644 index 0000000..a3bbcdd --- /dev/null +++ b/mapper_test.go @@ -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) + } + } +}