diff options
author | Michael Muré <batolettre@gmail.com> | 2020-03-14 16:47:38 +0100 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2020-03-28 17:13:27 +0100 |
commit | 5e4dc87ffec7f87bbf3ebfcf256777ad773e8450 (patch) | |
tree | 04553cfb7ab8ea279c7415586ce1d0fe5c819996 | |
parent | 58abc6b0a35b679ac0c34579ff1cb53c8fa71af4 (diff) | |
download | git-bug-5e4dc87ffec7f87bbf3ebfcf256777ad773e8450.tar.gz |
cache: replace the all-in-one query parser by a complete one with AST/lexer/parser
-rw-r--r-- | cache/filter.go | 47 | ||||
-rw-r--r-- | cache/query.go | 172 | ||||
-rw-r--r-- | cache/query_test.go | 41 | ||||
-rw-r--r-- | cache/repo_cache.go | 20 | ||||
-rw-r--r-- | cache/repo_cache_test.go | 5 | ||||
-rw-r--r-- | cache/sorting.go | 18 | ||||
-rw-r--r-- | commands/ls.go | 57 | ||||
-rw-r--r-- | graphql/resolvers/repo.go | 13 | ||||
-rw-r--r-- | query/ast/ast.go | 45 | ||||
-rw-r--r-- | query/lexer.go | 71 | ||||
-rw-r--r-- | query/lexer_test.go | 45 | ||||
-rw-r--r-- | query/parser.go | 100 | ||||
-rw-r--r-- | query/parser_test.go | 98 | ||||
-rw-r--r-- | termui/bug_table.go | 8 | ||||
-rw-r--r-- | termui/termui.go | 5 |
15 files changed, 451 insertions, 294 deletions
diff --git a/cache/filter.go b/cache/filter.go index ebe774ac..021962a7 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -5,6 +5,7 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/query/ast" ) // resolver has the resolving functions needed by filters. @@ -17,15 +18,10 @@ type resolver interface { type Filter func(excerpt *BugExcerpt, resolver resolver) bool // StatusFilter return a Filter that match a bug status -func StatusFilter(query string) (Filter, error) { - status, err := bug.StatusFromString(query) - if err != nil { - return nil, err - } - +func StatusFilter(status bug.Status) Filter { return func(excerpt *BugExcerpt, resolver resolver) bool { return excerpt.Status == status - }, nil + } } // AuthorFilter return a Filter that match a bug author @@ -116,8 +112,8 @@ func NoLabelFilter() Filter { } } -// Filters is a collection of Filter that implement a complex filter -type Filters struct { +// Matcher is a collection of Filter that implement a complex filter +type Matcher struct { Status []Filter Author []Filter Actor []Filter @@ -127,8 +123,35 @@ type Filters struct { NoFilters []Filter } +// compileMatcher transform an ast.Filters into a specialized matcher +// for the cache. +func compileMatcher(filters ast.Filters) *Matcher { + result := &Matcher{} + + for _, value := range filters.Status { + result.Status = append(result.Status, StatusFilter(value)) + } + for _, value := range filters.Author { + result.Author = append(result.Author, AuthorFilter(value)) + } + for _, value := range filters.Actor { + result.Actor = append(result.Actor, ActorFilter(value)) + } + for _, value := range filters.Participant { + result.Participant = append(result.Participant, ParticipantFilter(value)) + } + for _, value := range filters.Label { + result.Label = append(result.Label, LabelFilter(value)) + } + for _, value := range filters.Title { + result.Title = append(result.Title, TitleFilter(value)) + } + + return result +} + // Match check if a bug match the set of filters -func (f *Filters) Match(excerpt *BugExcerpt, resolver resolver) bool { +func (f *Matcher) Match(excerpt *BugExcerpt, resolver resolver) bool { if match := f.orMatch(f.Status, excerpt, resolver); !match { return false } @@ -161,7 +184,7 @@ func (f *Filters) Match(excerpt *BugExcerpt, resolver resolver) bool { } // Check if any of the filters provided match the bug -func (*Filters) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { +func (*Matcher) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { if len(filters) == 0 { return true } @@ -175,7 +198,7 @@ func (*Filters) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver } // Check if all of the filters provided match the bug -func (*Filters) andMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { +func (*Matcher) andMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { if len(filters) == 0 { return true } diff --git a/cache/query.go b/cache/query.go deleted file mode 100644 index 967c18d6..00000000 --- a/cache/query.go +++ /dev/null @@ -1,172 +0,0 @@ -package cache - -import ( - "fmt" - "strings" - "unicode" -) - -type Query struct { - Filters - OrderBy - OrderDirection -} - -// Return an identity query with default sorting (creation-desc) -func NewQuery() *Query { - return &Query{ - OrderBy: OrderByCreation, - OrderDirection: OrderDescending, - } -} - -// ParseQuery parse a query DSL -// -// Ex: "status:open author:descartes sort:edit-asc" -// -// Supported filter qualifiers and syntax are described in docs/queries.md -func ParseQuery(query string) (*Query, error) { - fields := splitQuery(query) - - result := &Query{ - OrderBy: OrderByCreation, - OrderDirection: OrderDescending, - } - - sortingDone := false - - for _, field := range fields { - split := strings.Split(field, ":") - if len(split) != 2 { - return nil, fmt.Errorf("can't parse \"%s\"", field) - } - - qualifierName := split[0] - qualifierQuery := removeQuote(split[1]) - - switch qualifierName { - case "status", "state": - f, err := StatusFilter(qualifierQuery) - if err != nil { - return nil, err - } - result.Status = append(result.Status, f) - - case "author": - f := AuthorFilter(qualifierQuery) - result.Author = append(result.Author, f) - - case "actor": - f := ActorFilter(qualifierQuery) - result.Actor = append(result.Actor, f) - - case "participant": - f := ParticipantFilter(qualifierQuery) - result.Participant = append(result.Participant, f) - - case "label": - f := LabelFilter(qualifierQuery) - result.Label = append(result.Label, f) - - case "title": - f := TitleFilter(qualifierQuery) - result.Title = append(result.Title, f) - - case "no": - err := result.parseNoFilter(qualifierQuery) - if err != nil { - return nil, err - } - - case "sort": - if sortingDone { - return nil, fmt.Errorf("multiple sorting") - } - - err := result.parseSorting(qualifierQuery) - if err != nil { - return nil, err - } - - sortingDone = true - - default: - return nil, fmt.Errorf("unknown qualifier name %s", qualifierName) - } - } - - return result, nil -} - -func splitQuery(query string) []string { - lastQuote := rune(0) - f := func(c rune) bool { - switch { - case c == lastQuote: - lastQuote = rune(0) - return false - case lastQuote != rune(0): - return false - case unicode.In(c, unicode.Quotation_Mark): - lastQuote = c - return false - default: - return unicode.IsSpace(c) - } - } - - return strings.FieldsFunc(query, f) -} - -func removeQuote(field string) string { - if len(field) >= 2 { - if field[0] == '"' && field[len(field)-1] == '"' { - return field[1 : len(field)-1] - } - } - return field -} - -func (q *Query) parseNoFilter(query string) error { - switch query { - case "label": - q.NoFilters = append(q.NoFilters, NoLabelFilter()) - default: - return fmt.Errorf("unknown \"no\" filter %s", query) - } - - return nil -} - -func (q *Query) parseSorting(query string) error { - switch query { - // default ASC - case "id-desc": - q.OrderBy = OrderById - q.OrderDirection = OrderDescending - case "id", "id-asc": - q.OrderBy = OrderById - q.OrderDirection = OrderAscending - - // default DESC - case "creation", "creation-desc": - q.OrderBy = OrderByCreation - q.OrderDirection = OrderDescending - case "creation-asc": - q.OrderBy = OrderByCreation - q.OrderDirection = OrderAscending - - // default DESC - case "edit", "edit-desc": - q.OrderBy = OrderByEdit - q.OrderDirection = OrderDescending - case "edit-asc": - q.OrderBy = OrderByEdit - q.OrderDirection = OrderAscending - - default: - return fmt.Errorf("unknown sorting %s", query) - } - - return nil -} diff --git a/cache/query_test.go b/cache/query_test.go deleted file mode 100644 index 9ae62ac4..00000000 --- a/cache/query_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package cache - -import "testing" - -func TestQueryParse(t *testing.T) { - - var tests = []struct { - input string - ok bool - }{ - {"gibberish", false}, - - {"status:", false}, - - {"status:open", true}, - {"status:closed", true}, - {"status:unknown", false}, - - {"author:rene", true}, - {`author:"René Descartes"`, true}, - - {"actor:bernhard", true}, - {"participant:leonhard", true}, - - {"label:hello", true}, - {`label:"Good first issue"`, true}, - - {"title:titleOne", true}, - {`title:"Bug titleTwo"`, true}, - - {"sort:edit", true}, - {"sort:unknown", false}, - } - - for _, test := range tests { - _, err := ParseQuery(test.input) - if (err == nil) != test.ok { - t.Fatalf("Unexpected parse result, expected: %v, err: %v", test.ok, err) - } - } -} diff --git a/cache/repo_cache.go b/cache/repo_cache.go index d859a966..6546b15e 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -18,6 +18,7 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/query/ast" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/util/process" @@ -525,7 +526,7 @@ func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, erro } // QueryBugs return the id of all Bug matching the given Query -func (c *RepoCache) QueryBugs(query *Query) []entity.Id { +func (c *RepoCache) QueryBugs(query *ast.Query) []entity.Id { c.muBug.RLock() defer c.muBug.RUnlock() @@ -533,10 +534,12 @@ func (c *RepoCache) QueryBugs(query *Query) []entity.Id { return c.AllBugsIds() } + matcher := compileMatcher(query.Filters) + var filtered []*BugExcerpt for _, excerpt := range c.bugExcerpts { - if query.Match(excerpt, c) { + if matcher.Match(excerpt, c) { filtered = append(filtered, excerpt) } } @@ -544,18 +547,23 @@ func (c *RepoCache) QueryBugs(query *Query) []entity.Id { var sorter sort.Interface switch query.OrderBy { - case OrderById: + case ast.OrderById: sorter = BugsById(filtered) - case OrderByCreation: + case ast.OrderByCreation: sorter = BugsByCreationTime(filtered) - case OrderByEdit: + case ast.OrderByEdit: sorter = BugsByEditTime(filtered) default: panic("missing sort type") } - if query.OrderDirection == OrderDescending { + switch query.OrderDirection { + case ast.OrderAscending: + // Nothing to do + case ast.OrderDescending: sorter = sort.Reverse(sorter) + default: + panic("missing sort direction") } sort.Sort(sorter) diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index c3bd3cc4..51393dfd 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/MichaelMure/git-bug/query" "github.com/MichaelMure/git-bug/repository" ) @@ -68,9 +69,9 @@ func TestCache(t *testing.T) { require.NoError(t, err) // Querying - query, err := ParseQuery("status:open author:descartes sort:edit-asc") + q, err := query.Parse("status:open author:descartes sort:edit-asc") require.NoError(t, err) - require.Len(t, cache.QueryBugs(query), 2) + require.Len(t, cache.QueryBugs(q), 2) // Close require.NoError(t, cache.Close()) diff --git a/cache/sorting.go b/cache/sorting.go deleted file mode 100644 index 19034a9d..00000000 --- a/cache/sorting.go +++ /dev/null @@ -1,18 +0,0 @@ -package cache - -type OrderBy int - -const ( - _ OrderBy = iota - OrderById - OrderByCreation - OrderByEdit -) - -type OrderDirection int - -const ( - _ OrderDirection = iota - OrderAscending - OrderDescending -) diff --git a/commands/ls.go b/commands/ls.go index 70a948e6..aff56f61 100644 --- a/commands/ls.go +++ b/commands/ls.go @@ -7,7 +7,10 @@ import ( text "github.com/MichaelMure/go-term-text" "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/query" + "github.com/MichaelMure/git-bug/query/ast" "github.com/MichaelMure/git-bug/util/colors" "github.com/MichaelMure/git-bug/util/interrupt" ) @@ -32,21 +35,21 @@ func runLsBug(cmd *cobra.Command, args []string) error { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - var query *cache.Query + var q *ast.Query if len(args) >= 1 { - query, err = cache.ParseQuery(strings.Join(args, " ")) + q, err = query.Parse(strings.Join(args, " ")) if err != nil { return err } } else { - query, err = lsQueryFromFlags() + q, err = lsQueryFromFlags() if err != nil { return err } } - allIds := backend.QueryBugs(query) + allIds := backend.QueryBugs(q) for _, id := range allIds { b, err := backend.ResolveBugExcerpt(id) @@ -96,47 +99,37 @@ func runLsBug(cmd *cobra.Command, args []string) error { return nil } -// Transform the command flags into a query -func lsQueryFromFlags() (*cache.Query, error) { - query := cache.NewQuery() +// Transform the command flags into an ast.Query +func lsQueryFromFlags() (*ast.Query, error) { + q := ast.NewQuery() - for _, status := range lsStatusQuery { - f, err := cache.StatusFilter(status) + for _, str := range lsStatusQuery { + status, err := bug.StatusFromString(str) if err != nil { return nil, err } - query.Status = append(query.Status, f) + q.Status = append(q.Status, status) } - for _, title := range lsTitleQuery { - f := cache.TitleFilter(title) - query.Title = append(query.Title, f) + q.Title = append(q.Title, title) } - for _, author := range lsAuthorQuery { - f := cache.AuthorFilter(author) - query.Author = append(query.Author, f) + q.Author = append(q.Author, author) } - for _, actor := range lsActorQuery { - f := cache.ActorFilter(actor) - query.Actor = append(query.Actor, f) + q.Actor = append(q.Actor, actor) } - for _, participant := range lsParticipantQuery { - f := cache.ParticipantFilter(participant) - query.Participant = append(query.Participant, f) + q.Participant = append(q.Participant, participant) } - for _, label := range lsLabelQuery { - f := cache.LabelFilter(label) - query.Label = append(query.Label, f) + q.Label = append(q.Label, label) } for _, no := range lsNoQuery { switch no { case "label": - query.NoFilters = append(query.NoFilters, cache.NoLabelFilter()) + q.NoLabel = true default: return nil, fmt.Errorf("unknown \"no\" filter %s", no) } @@ -144,25 +137,25 @@ func lsQueryFromFlags() (*cache.Query, error) { switch lsSortBy { case "id": - query.OrderBy = cache.OrderById + q.OrderBy = ast.OrderById case "creation": - query.OrderBy = cache.OrderByCreation + q.OrderBy = ast.OrderByCreation case "edit": - query.OrderBy = cache.OrderByEdit + q.OrderBy = ast.OrderByEdit default: return nil, fmt.Errorf("unknown sort flag %s", lsSortBy) } switch lsSortDirection { case "asc": - query.OrderDirection = cache.OrderAscending + q.OrderDirection = ast.OrderAscending case "desc": - query.OrderDirection = cache.OrderDescending + q.OrderDirection = ast.OrderDescending default: return nil, fmt.Errorf("unknown sort direction %s", lsSortDirection) } - return query, nil + return q, nil } var lsCmd = &cobra.Command{ diff --git a/graphql/resolvers/repo.go b/graphql/resolvers/repo.go index d090544d..dde1dcf6 100644 --- a/graphql/resolvers/repo.go +++ b/graphql/resolvers/repo.go @@ -4,11 +4,12 @@ import ( "context" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/graphql/connections" "github.com/MichaelMure/git-bug/graphql/graph" "github.com/MichaelMure/git-bug/graphql/models" + "github.com/MichaelMure/git-bug/query" + "github.com/MichaelMure/git-bug/query/ast" ) var _ graph.RepositoryResolver = &repoResolver{} @@ -28,19 +29,19 @@ func (repoResolver) AllBugs(_ context.Context, obj *models.Repository, after *st Last: last, } - var query *cache.Query + var q *ast.Query if queryStr != nil { - query2, err := cache.ParseQuery(*queryStr) + query2, err := query.Parse(*queryStr) if err != nil { return nil, err } - query = query2 + q = query2 } else { - query = cache.NewQuery() + q = ast.NewQuery() } // Simply pass a []string with the ids to the pagination algorithm - source := obj.Repo.QueryBugs(query) + source := obj.Repo.QueryBugs(q) // The edger create a custom edge holding just the id edger := func(id entity.Id, offset int) connections.Edge { diff --git a/query/ast/ast.go b/query/ast/ast.go new file mode 100644 index 00000000..fe77abf9 --- /dev/null +++ b/query/ast/ast.go @@ -0,0 +1,45 @@ +package ast + +import "github.com/MichaelMure/git-bug/bug" + +type Query struct { + Filters + OrderBy + OrderDirection +} + +// NewQuery return an identity query with the default sorting (creation-desc). +func NewQuery() *Query { + return &Query{ + OrderBy: OrderByCreation, + OrderDirection: OrderDescending, + } +} + +// Filters is a collection of Filter that implement a complex filter +type Filters struct { + Status []bug.Status + Author []string + Actor []string + Participant []string + Label []string + Title []string + NoLabel bool +} + +type OrderBy int + +const ( + _ OrderBy = iota + OrderById + OrderByCreation + OrderByEdit +) + +type OrderDirection int + +const ( + _ OrderDirection = iota + OrderAscending + OrderDescending +) diff --git a/query/lexer.go b/query/lexer.go new file mode 100644 index 00000000..ec05f44e --- /dev/null +++ b/query/lexer.go @@ -0,0 +1,71 @@ +package query + +import ( + "fmt" + "strings" + "unicode" +) + +type token struct { + qualifier string + value string +} + +// TODO: this lexer implementation behave badly with unmatched quotes. +// A hand written one would be better instead of relying on strings.FieldsFunc() + +// tokenize parse and break a input into tokens ready to be +// interpreted later by a parser to get the semantic. +func tokenize(query string) ([]token, error) { + fields := splitQuery(query) + + var tokens []token + for _, field := range fields { + split := strings.Split(field, ":") + if len(split) != 2 { + return nil, fmt.Errorf("can't tokenize \"%s\"", field) + } + + if len(split[0]) == 0 { + return nil, fmt.Errorf("can't tokenize \"%s\": empty qualifier", field) + } + if len(split[1]) == 0 { + return nil, fmt.Errorf("empty value for qualifier \"%s\"", split[0]) + } + + tokens = append(tokens, token{ + qualifier: split[0], + value: removeQuote(split[1]), + }) + } + return tokens, nil +} + +func splitQuery(query string) []string { + lastQuote := rune(0) + f := func(c rune) bool { + switch { + case c == lastQuote: + lastQuote = rune(0) + return false + case lastQuote != rune(0): + return false + case unicode.In(c, unicode.Quotation_Mark): + lastQuote = c + return false + default: + return unicode.IsSpace(c) + } + } + + return strings.FieldsFunc(query, f) +} + +func removeQuote(field string) string { + if len(field) >= 2 { + if field[0] == '"' && field[len(field)-1] == '"' { + return field[1 : len(field)-1] + } + } + return field +} diff --git a/query/lexer_test.go b/query/lexer_test.go new file mode 100644 index 00000000..922e3fc9 --- /dev/null +++ b/query/lexer_test.go @@ -0,0 +1,45 @@ +package query + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTokenize(t *testing.T) { + var tests = []struct { + input string + tokens []token + }{ + {"gibberish", nil}, + {"status:", nil}, + {":value", nil}, + + {"status:open", []token{{"status", "open"}}}, + {"status:closed", []token{{"status", "closed"}}}, + + {"author:rene", []token{{"author", "rene"}}}, + {`author:"René Descartes"`, []token{{"author", "René Descartes"}}}, + + { + `status:open status:closed author:rene author:"René Descartes"`, + []token{ + {"status", "open"}, + {"status", "closed"}, + {"author", "rene"}, + {"author", "René Descartes"}, + }, + }, + } + + for _, tc := range tests { + tokens, err := tokenize(tc.input) + if tc.tokens == nil { + assert.Error(t, err) + assert.Nil(t, tokens) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.tokens, tokens) + } + } +} diff --git a/query/parser.go b/query/parser.go new file mode 100644 index 00000000..89893b60 --- /dev/null +++ b/query/parser.go @@ -0,0 +1,100 @@ +package query + +import ( + "fmt" + + "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/query/ast" +) + +// Parse parse a query DSL +// +// Ex: "status:open author:descartes sort:edit-asc" +// +// Supported filter qualifiers and syntax are described in docs/queries.md +func Parse(query string) (*ast.Query, error) { + tokens, err := tokenize(query) + if err != nil { + return nil, err + } + + q := &ast.Query{ + OrderBy: ast.OrderByCreation, + OrderDirection: ast.OrderDescending, + } + sortingDone := false + + for _, t := range tokens { + switch t.qualifier { + case "status", "state": + status, err := bug.StatusFromString(t.value) + if err != nil { + return nil, err + } + q.Status = append(q.Status, status) + case "author": + q.Author = append(q.Author, t.value) + case "actor": + q.Actor = append(q.Actor, t.value) + case "participant": + q.Participant = append(q.Participant, t.value) + case "label": + q.Label = append(q.Label, t.value) + case "title": + q.Title = append(q.Title, t.value) + case "no": + switch t.value { + case "label": + q.NoLabel = true + default: + return nil, fmt.Errorf("unknown \"no\" filter \"%s\"", t.value) + } + case "sort": + if sortingDone { + return nil, fmt.Errorf("multiple sorting") + } + err = parseSorting(q, t.value) + if err != nil { + return nil, err + } + sortingDone = true + + default: + return nil, fmt.Errorf("unknown qualifier \"%s\"", t.qualifier) + } + } + return q, nil +} + +func parseSorting(q *ast.Query, value string) error { + switch value { + // default ASC + case "id-desc": + q.OrderBy = ast.OrderById + q.OrderDirection = ast.OrderDescending + case "id", "id-asc": + q.OrderBy = ast.OrderById + q.OrderDirection = ast.OrderAscending + + // default DESC + case "creation", "creation-desc": + q.OrderBy = ast.OrderByCreation + q.OrderDirection = ast.OrderDescending + case "creation-asc": + q.OrderBy = ast.OrderByCreation + q.OrderDirection = ast.OrderAscending + + // default DESC + case "edit", "edit-desc": + q.OrderBy = ast.OrderByEdit + q.OrderDirection = ast.OrderDescending + case "edit-asc": + q.OrderBy = ast.OrderByEdit + q.OrderDirection = ast.OrderAscending + + default: + return fmt.Errorf("unknown sorting %s", value) + } + + return nil +} diff --git a/query/parser_test.go b/query/parser_test.go new file mode 100644 index 00000000..065e647a --- /dev/null +++ b/query/parser_test.go @@ -0,0 +1,98 @@ +package query + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/query/ast" +) + +func TestParse(t *testing.T) { + var tests = []struct { + input string + output *ast.Query + }{ + {"gibberish", nil}, + {"status:", nil}, + {":value", nil}, + + {"status:open", &ast.Query{ + Filters: ast.Filters{Status: []bug.Status{bug.OpenStatus}}, + }}, + {"status:closed", &ast.Query{ + Filters: ast.Filters{Status: []bug.Status{bug.ClosedStatus}}, + }}, + {"status:unknown", nil}, + + {"author:rene", &ast.Query{ + Filters: ast.Filters{Author: []string{"rene"}}, + }}, + {`author:"René Descartes"`, &ast.Query{ + Filters: ast.Filters{Author: []string{"René Descartes"}}, + }}, + + {"actor:bernhard", &ast.Query{ + Filters: ast.Filters{Actor: []string{"bernhard"}}, + }}, + {"participant:leonhard", &ast.Query{ + Filters: ast.Filters{Participant: []string{"leonhard"}}, + }}, + + {"label:hello", &ast.Query{ + Filters: ast.Filters{Label: []string{"hello"}}, + }}, + {`label:"Good first issue"`, &ast.Query{ + Filters: ast.Filters{Label: []string{"Good first issue"}}, + }}, + + {"title:titleOne", &ast.Query{ + Filters: ast.Filters{Title: []string{"titleOne"}}, + }}, + {`title:"Bug titleTwo"`, &ast.Query{ + Filters: ast.Filters{Title: []string{"Bug titleTwo"}}, + }}, + + {"no:label", &ast.Query{ + Filters: ast.Filters{NoLabel: true}, + }}, + + {"sort:edit", &ast.Query{ + OrderBy: ast.OrderByEdit, + }}, + {"sort:unknown", nil}, + + {`status:open author:"René Descartes" participant:leonhard label:hello label:"Good first issue" sort:edit-desc`, + &ast.Query{ + Filters: ast.Filters{ + Status: []bug.Status{bug.OpenStatus}, + Author: []string{"René Descartes"}, + Participant: []string{"leonhard"}, + Label: []string{"hello", "Good first issue"}, + }, + OrderBy: ast.OrderByEdit, + OrderDirection: ast.OrderDescending, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + query, err := Parse(tc.input) + if tc.output == nil { + assert.Error(t, err) + assert.Nil(t, query) + } else { + assert.NoError(t, err) + if tc.output.OrderBy != 0 { + assert.Equal(t, tc.output.OrderBy, query.OrderBy) + } + if tc.output.OrderDirection != 0 { + assert.Equal(t, tc.output.OrderDirection, query.OrderDirection) + } + assert.Equal(t, tc.output.Filters, query.Filters) + } + }) + } +} diff --git a/termui/bug_table.go b/termui/bug_table.go index 41aa4e83..74f92826 100644 --- a/termui/bug_table.go +++ b/termui/bug_table.go @@ -12,6 +12,8 @@ import ( "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/query" + "github.com/MichaelMure/git-bug/query/ast" "github.com/MichaelMure/git-bug/util/colors" ) @@ -26,7 +28,7 @@ const defaultQuery = "status:open" type bugTable struct { repo *cache.RepoCache queryStr string - query *cache.Query + query *ast.Query allIds []entity.Id excerpts []*cache.BugExcerpt pageCursor int @@ -34,14 +36,14 @@ type bugTable struct { } func newBugTable(c *cache.RepoCache) *bugTable { - query, err := cache.ParseQuery(defaultQuery) + q, err := query.Parse(defaultQuery) if err != nil { panic(err) } return &bugTable{ repo: c, - query: query, + query: q, queryStr: defaultQuery, pageCursor: 0, selectCursor: 0, diff --git a/termui/termui.go b/termui/termui.go index 1ef960de..96b7583c 100644 --- a/termui/termui.go +++ b/termui/termui.go @@ -12,6 +12,7 @@ import ( "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/input" + "github.com/MichaelMure/git-bug/query" ) var errTerminateMainloop = errors.New("terminate gocui mainloop") @@ -336,12 +337,12 @@ func editQueryWithEditor(bt *bugTable) error { bt.queryStr = queryStr - query, err := cache.ParseQuery(queryStr) + q, err := query.Parse(queryStr) if err != nil { ui.msgPopup.Activate(msgPopupErrorTitle, err.Error()) } else { - bt.query = query + bt.query = q } initGui(nil) |