diff --git a/.gitignore b/.gitignore index bfa6551..a2a9b72 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ notes +*.todo +*.vscode \ No newline at end of file diff --git a/integration/Dockerfile b/integration/Dockerfile index fbdaf21..df354a2 100644 --- a/integration/Dockerfile +++ b/integration/Dockerfile @@ -3,3 +3,4 @@ FROM golang RUN go get github.com/mailru/easyjson RUN go get github.com/jinzhu/gorm RUN go get github.com/go-sql-driver/mysql +RUN go get gopkg.in/mgo.v2/bson diff --git a/integration/rql_test.go b/integration/rql_test.go index afb689d..21eaf53 100644 --- a/integration/rql_test.go +++ b/integration/rql_test.go @@ -8,7 +8,6 @@ import ( "time" "github.com/a8m/rql" - _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" ) diff --git a/rql.go b/rql.go index 2646e6e..64e45b3 100644 --- a/rql.go +++ b/rql.go @@ -12,12 +12,15 @@ import ( "sync" "time" "unicode" + + "gopkg.in/mgo.v2/bson" ) //go:generate easyjson -omit_empty -disallow_unknown_fields -snake_case rql.go // Query is the decoded result of the user input. //easyjson:json +// TODO: Need to add Selector for Mongodb operations. type Query struct { // Limit must be greater than 0 and less than or equal to `LimitMaxValue`. Limit int `json:"limit,omitempty"` @@ -82,6 +85,25 @@ type Params struct { FilterArgs []interface{} } +// MgoParams contains details for mongodb query operations. +type MgoParams struct { + //Limit represents the number of documents to be retruned. + Limit int + + //Skip represents the number of documents to be skiped. + Skip int + + // Sort the documents. + // example: "name,-age". + Sort string + + //Select the required keys and values for the document. + Select bson.M + + //Find the documents based on the query. + Find bson.M +} + // ParseError is type of error returned when there is a parsing problem. type ParseError struct { msg string @@ -150,17 +172,20 @@ func (p *Parser) Parse(b []byte) (pr *Params, err error) { pr = nil } }() + q := new(Query) must(q.UnmarshalJSON(b), "decoding buffer to Query") pr = &Params{ Limit: p.DefaultLimit, } + expect(q.Offset >= 0, "offset must be greater than or equal to 0") pr.Offset = q.Offset if q.Limit != 0 { expect(q.Limit > 0 && q.Limit <= p.LimitMaxValue, "limit must be greater than 0 and less than or equal to %d", p.Model) pr.Limit = q.Limit } + ps := p.newParseState() ps.and(q.Filter) pr.FilterExp = ps.String() @@ -170,6 +195,42 @@ func (p *Parser) Parse(b []byte) (pr *Params, err error) { return } +// ParseMgo parses the given buffer into a Param object. It returns an error +// if the JSON is invalid, or its values don't follow the schema of rql. +func (p *Parser) ParseMgo(b []byte) (pr *MgoParams, err error) { + defer func() { + if e := recover(); e != nil { + perr, ok := e.(ParseError) + if !ok { + panic(e) + } + err = perr + pr = nil + } + }() + + q := new(Query) + must(q.UnmarshalJSON(b), "decoding buffer to Query") + pr = &MgoParams{ + Limit: p.DefaultLimit, + } + + expect(q.Offset >= 0, "offset must be greater than or equal to 0") + pr.Skip = q.Offset + if q.Limit != 0 { + expect(q.Limit > 0 && q.Limit <= p.LimitMaxValue, "limit must be greater than 0 and less than or equal to %d", p.Model) + pr.Limit = q.Limit + } + + if q.Filter != nil { + expect(len(q.Filter) > 0, "filter shouldn't be empty") + } + + // TODO: Need to perform type converstion. + pr.Find = q.Filter + return +} + // 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: // diff --git a/rql_test.go b/rql_test.go index 7aa6673..cae6592 100644 --- a/rql_test.go +++ b/rql_test.go @@ -2,10 +2,13 @@ package rql import ( "database/sql" + "log" "reflect" "strings" "testing" "time" + + "gopkg.in/mgo.v2/bson" ) func TestInit(t *testing.T) { @@ -788,6 +791,57 @@ func TestParse(t *testing.T) { } } +func TestParseMgo(t *testing.T) { + tests := []struct { + name string + conf Config + input []byte + wantErr bool + wantOut *MgoParams + }{ + { + //BUG: need to perform type converison, want int but got float64. + name: "simple test", + conf: Config{ + Model: new(struct { + Age int `rql:"filter"` + Name string `rql:"filter"` + }), + DefaultLimit: 25, + }, + input: []byte(`{ + "filter": { + "name": "foo", + "age": 12 + } + }`), + wantOut: &MgoParams{ + Limit: 25, + Find: bson.M{ + "name": "foo", + "age": 12, + }, + }, + }, + } + + 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) + } + + _, err = p.ParseMgo(tt.input) + if tt.wantErr != (err != nil) { + t.Fatalf("want: %v\ngot:%v\nerr: %v", tt.wantErr, err != nil, err) + } + + // assertMgoParams(t, out, tt.wantOut) + }) + } +} + // AssertQueryEqual tests if two query input are equal. // TODO: improve this in the future. func assertParams(t *testing.T, got *Params, want *Params) { @@ -811,6 +865,41 @@ func assertParams(t *testing.T, got *Params, want *Params) { } } +// assertMgoParasm tests if two query input are equal for Mongodb query output. +// TODO: need to add Select & Find. +func assertMgoParams(t *testing.T, got *MgoParams, want *MgoParams) { + if got == nil && want == nil { + return + } + + if got.Limit != want.Limit { + t.Fatalf("limit: got: %v want %v", got.Limit, want.Limit) + } + + if got.Skip != want.Skip { + t.Fatalf("skip: got: %v want %v", got.Limit, want.Limit) + } + + if got.Sort != want.Sort { + t.Fatalf("sort: got: %q want %q", got.Sort, want.Sort) + } + + for wantKey, wantVal := range want.Select { + if gotVal, ok := got.Select[wantKey]; !ok || gotVal != wantVal { + t.Fatalf("select: got: %v want %v", gotVal, wantVal) + } + } + + for wantKey, wantVal := range want.Find { + if gotVal, ok := got.Find[wantKey]; !ok || gotVal != wantVal { + log.Print(reflect.ValueOf(gotVal).Type()) + log.Print(reflect.ValueOf(wantVal).Type()) + t.Fatalf("find: got: %v want %v", gotVal, wantVal) + } + } + +} + func equalArgs(a, b []interface{}) bool { seen := make([]bool, len(b)) for _, arg1 := range a {