diff --git a/rql.go b/rql.go index 7775487..5137765 100644 --- a/rql.go +++ b/rql.go @@ -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`. @@ -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 @@ -105,7 +105,7 @@ func (p ParseError) Error() string { } // field is a configuration of a struct field. -type field struct { +type FieldMeta struct { // Name of the column. Name string // Has a "sort" option in the tag. @@ -114,6 +114,9 @@ type field struct { Filterable bool // All supported operators for this field. FilterOps map[string]bool +} +type Field struct { + *FieldMeta // Validation for the type. for example, unit8 greater than or equal to 0. ValidateFn func(interface{}) error // ConvertFn converts the given value to the type value. @@ -124,7 +127,7 @@ type field struct { // It is safe for concurrent use by multiple goroutines except for configuration changes. type Parser struct { Config - fields map[string]*field + fields map[string]*Field } // NewParser creates a new Parser. it fails if the configuration is invalid. @@ -134,7 +137,7 @@ func NewParser(c Config) (*Parser, error) { } p := &Parser{ Config: c, - fields: make(map[string]*field), + fields: make(map[string]*Field), } if err := p.init(); err != nil { return nil, err @@ -142,6 +145,23 @@ func NewParser(c Config) (*Parser, error) { return p, nil } +// Does not use config.Model, gets config from Fields) +func NewParserF(c Config, fields []*Field) (*Parser, error) { + if err := c.defaults(); err != nil { + return nil, err + } + + m := make(map[string]*Field, len(fields)) + for _, v := range fields { + m[v.Name] = v + } + p := &Parser{ + Config: c, + fields: m, + } + return p, nil +} + // MustNewParser is like NewParser but panics if the configuration is invalid. // It simplifies safe initialization of global variables holding a resource parser. func MustNewParser(c Config) *Parser { @@ -197,13 +217,20 @@ func (p *Parser) ParseQuery(q *Query) (pr *Params, err error) { return } +func (p *Parser) GetFields() []*Field { + fields := make([]*Field, 0, len(p.fields)) + for _, v := range p.fields { + fields = append(fields, v) + } + return fields +} + // Column is the default function that converts field name into a database column. // It used to convert the struct fields into their database names. For example: // // Username => username // FullName => full_name // HTTPCode => http_code -// func Column(s string) string { var b strings.Builder for i := 0; i < len(s); i++ { @@ -257,10 +284,12 @@ func (p *Parser) init() error { // parseField parses the given struct field tag, and add a rule // 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), - CovertFn: valueFn, - FilterOps: make(map[string]bool), + f := &Field{ + FieldMeta: &FieldMeta{ + Name: p.ColumnFn(sf.Name), + FilterOps: make(map[string]bool), + }, + CovertFn: valueFn, } layout := time.RFC3339 opts := strings.Split(sf.Tag.Get(p.TagName), ",") @@ -438,7 +467,7 @@ func (p *parseState) relOp(op Op, terms []interface{}) { } } -func (p *parseState) field(f *field, v interface{}) { +func (p *parseState) field(f *Field, v interface{}) { terms, ok := v.(map[string]interface{}) // default equality check. if !ok { diff --git a/rql_test.go b/rql_test.go index f691219..41a59e5 100644 --- a/rql_test.go +++ b/rql_test.go @@ -1021,3 +1021,58 @@ func mustParseTime(layout, s string) time.Time { t, _ := time.Parse(layout, s) return t } + +func TestGetFields(t *testing.T) { + tests := []struct { + name string + conf Config + wantOut []*Field + }{ + { + name: "get fields", + conf: Config{ + Model: struct { + SomeName string `rql:"filter"` + }{}, + }, + wantOut: []*Field{ + &Field{ + FieldMeta: &FieldMeta{ + Name: "some_name", + Sortable: false, + Filterable: true, + }, + // Column: "some_name", + // AvailableOps: []string{"$eq", "$neq", "$lt", "$lte", "$gt", "$gte", "$like"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p, err := NewParser(tt.conf) + if err != nil { + t.Fatalf("failed to build parser: %v", err) + } + out := p.GetFields() + assertFieldsEqual(t, out, tt.wantOut) + }) + } +} + +func assertFieldsEqual(t *testing.T, got []*Field, want []*Field) { + if len(got) != len(want) { + t.Fatalf("got %v, wanted %v", got, want) + } + for i := 0; i < len(got); i++ { + if got[i].Filterable != want[i].Filterable { + t.Fatalf("Filterable got:%v want: %v", got[i].Filterable, want[i].Filterable) + } + if got[i].Sortable != want[i].Sortable { + t.Fatalf("Sortable got:%v want: %v", got[i].Sortable, want[i].Sortable) + } + if got[i].Name != want[i].Name { + t.Fatalf("Name got:%v want: %v", got[i].Name, want[i].Name) + } + } +}