Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add custom field name support (NameFn) #50

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ type Config struct {
// }
//
FieldSep string
// NameFn is a function that translates the incoming filter query field name to the struct field name.
// For example, given the following query fields and their column names:
//
// fullName => "full_name"
// httpPort => "http_port"
//
// By default the field name is expected to match the column.
NameFn func(string, string) string
// ColumnFn is the function that translate the struct field string into a table column.
// For example, given the following fields and their column names:
//
Expand Down Expand Up @@ -150,7 +158,10 @@ func (c *Config) defaults() error {
c.Log = log.Printf
}
if c.ColumnFn == nil {
c.ColumnFn = Column
c.ColumnFn = ColumnFn
}
if c.NameFn == nil {
c.NameFn = NameFn
}
defaultString(&c.TagName, DefaultTagName)
defaultString(&c.OpPrefix, DefaultOpPrefix)
Expand Down
60 changes: 45 additions & 15 deletions rql.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
//go:generate easyjson -omit_empty -disallow_unknown_fields -snake_case rql.go

// Query is the decoded result of the user input.
//
//easyjson:json
type Query struct {
// Limit must be > 0 and <= to `LimitMaxValue`.
Expand Down Expand Up @@ -73,7 +74,6 @@ type Query struct {
// return nil, err
// }
// return users, nil
//
type Params struct {
// Limit represents the number of rows returned by the SELECT statement.
Limit int
Expand Down Expand Up @@ -106,8 +106,10 @@ func (p ParseError) Error() string {

// field is a configuration of a struct field.
type field struct {
// Name of the column.
// Name of the field.
Name string
// name of the column.
Column string
// Has a "sort" option in the tag.
Sortable bool
// Has a "filter" option in the tag.
Expand Down Expand Up @@ -203,8 +205,8 @@ func (p *Parser) ParseQuery(q *Query) (pr *Params, err error) {
// Username => username
// FullName => full_name
// HTTPCode => http_code
//
func Column(s string) string {
func ColumnFn(s string) string {
s = strings.Replace(s, ".", "_", -1)
var b strings.Builder
for i := 0; i < len(s); i++ {
r := rune(s[i])
Expand All @@ -221,6 +223,29 @@ func Column(s string) string {
return b.String()
}

func NameFn(str string, sep string) string {
var buf bytes.Buffer

for i, r := range str {
if r == '.' {
buf.WriteRune(r)
continue
}

if unicode.IsUpper(r) {
if i > 0 && str[i-1] != '_' && str[i-1] != '.' && !unicode.IsUpper(rune(str[i-1])) {
buf.WriteRune('_')
} else if i > 0 && unicode.IsUpper(rune(str[i-1])) && i+1 < len(str) && unicode.IsUpper(r) && unicode.IsLower(rune(str[i+1])) {
buf.WriteRune('_')
}
}

buf.WriteRune(unicode.ToLower(r))
}

return buf.String()
}

// init initializes the parser parsing state. it scans the fields
// in a breath-first-search order and for each one of the field calls parseField.
func (p *Parser) init() error {
Expand Down Expand Up @@ -258,7 +283,8 @@ func (p *Parser) init() error {
// in the parser according to its type and the options that were set on the tag.
func (p *Parser) parseField(sf reflect.StructField) error {
f := &field{
Name: p.ColumnFn(sf.Name),
Name: p.NameFn(sf.Name, p.FieldSep),
Column: p.ColumnFn(sf.Name),
CovertFn: valueFn,
FilterOps: make(map[string]bool),
}
Expand All @@ -271,7 +297,9 @@ func (p *Parser) parseField(sf reflect.StructField) error {
case s == "filter":
f.Filterable = true
case strings.HasPrefix(opt, "column"):
f.Name = strings.TrimPrefix(opt, "column=")
f.Column = strings.TrimPrefix(opt, "column=")
case strings.HasPrefix(opt, "name"):
f.Name = strings.TrimPrefix(opt, "name=")
case strings.HasPrefix(opt, "layout"):
layout = strings.TrimPrefix(opt, "layout=")
// if it's one of the standard layouts, like: RFC822 or Kitchen.
Expand All @@ -289,6 +317,7 @@ func (p *Parser) parseField(sf reflect.StructField) error {
p.Log("Ignoring unknown option %q in struct tag", opt)
}
}

var filterOps []Op
switch typ := indirect(sf.Type); typ.Kind() {
case reflect.Bool:
Expand Down Expand Up @@ -381,9 +410,10 @@ func (p *Parser) sort(fields []string) string {
orderBy = order
field = field[1:]
}
expect(p.fields[field] != nil, "unrecognized key %q for sorting", field)
expect(p.fields[field].Sortable, "field %q is not sortable", field)
colName := p.colName(field)
f := p.fields[field]
expect(f != nil, "unrecognized key %q for sorting", field)
expect(f.Sortable, "field %q is not sortable", field)
colName := f.Column
if orderBy != "" {
colName += " " + orderBy
}
Expand All @@ -408,8 +438,9 @@ func (p *parseState) and(f map[string]interface{}) {
expect(ok, "$and must be type array")
p.relOp(AND, terms)
case p.fields[k] != nil:
expect(p.fields[k].Filterable, "field %q is not filterable", k)
p.field(p.fields[k], v)
f := p.fields[k]
expect(f.Filterable, "field %q is not filterable", k)
p.field(f, v)
default:
expect(false, "unrecognized key %q for filtering", k)
}
Expand Down Expand Up @@ -443,7 +474,7 @@ func (p *parseState) field(f *field, v interface{}) {
// default equality check.
if !ok {
must(f.ValidateFn(v), "invalid datatype for field %q", f.Name)
p.WriteString(p.fmtOp(f.Name, EQ))
p.WriteString(p.fmtOp(f.Column, EQ))
p.values = append(p.values, f.CovertFn(v))
}
var i int
Expand All @@ -456,7 +487,7 @@ func (p *parseState) field(f *field, v interface{}) {
}
expect(f.FilterOps[opName], "can not apply op %q on field %q", opName, f.Name)
must(f.ValidateFn(opVal), "invalid datatype or format for field %q", f.Name)
p.WriteString(p.fmtOp(f.Name, Op(opName[1:])))
p.WriteString(p.fmtOp(f.Column, Op(opName[1:])))
p.values = append(p.values, f.CovertFn(opVal))
i++
}
Expand All @@ -468,8 +499,7 @@ func (p *parseState) field(f *field, v interface{}) {
// fmtOp create a string for the operation with a placeholder.
// for example: "name = ?", or "age >= ?".
func (p *Parser) fmtOp(field string, op Op) string {
colName := p.colName(field)
return colName + " " + op.SQL() + " ?"
return field + " " + op.SQL() + " ?"
}

// colName formats the query field to database column name in cases the user configured a custom
Expand Down
57 changes: 57 additions & 0 deletions rql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,63 @@ func TestParse(t *testing.T) {
}`),
wantErr: true,
},
{
name: "support name struct opt",
conf: Config{
Model: struct {
SomeName string `rql:"filter,name=someName"`
}{},
},
input: []byte(`{
"filter": {
"someName": {
"$eq": "someName"
}
}
}`),
wantOut: &Params{
Limit: 25,
FilterExp: "some_name = ?",
FilterArgs: []interface{}{"someName"},
},
},
{
name: "backwards compatibility to error with mismatching keys and no namefn",
conf: Config{
Model: struct {
SomeName string `rql:"filter"`
}{},
},
input: []byte(`{
"filter": {
"someName": {
"$eq": "someName"
}
}
}`),
wantErr: true,
},
{
name: "test nameFn works",
conf: Config{
Model: struct {
SomeName string `rql:"filter"`
}{},
NameFn: Column,
},
input: []byte(`{
"filter": {
"someName": {
"$eq": "someName"
}
}
}`),
wantOut: &Params{
Limit: 25,
FilterExp: "some_name = ?",
FilterArgs: []interface{}{"someName"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down