diff options
author | Michael Muré <batolettre@gmail.com> | 2021-02-27 20:39:27 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-27 20:39:27 +0100 |
commit | 22cc4cc043370bac945f92ecf343025ce7fdfe33 (patch) | |
tree | a0162d685c22d274dd4e5916d8cf46136c7dc0ea /query | |
parent | 10a259b6823e1234e5add1ee62935f259c39f803 (diff) | |
parent | fab626a7a663a8fa6ef27848bb63e91af812ab8c (diff) | |
download | git-bug-22cc4cc043370bac945f92ecf343025ce7fdfe33.tar.gz |
Merge pull request #568 from vmiklos/search-metadata
Add ability to search by arbitrary metadata
Diffstat (limited to 'query')
-rw-r--r-- | query/lexer.go | 108 | ||||
-rw-r--r-- | query/lexer_test.go | 28 | ||||
-rw-r--r-- | query/parser.go | 9 | ||||
-rw-r--r-- | query/parser_test.go | 19 | ||||
-rw-r--r-- | query/query.go | 7 |
5 files changed, 113 insertions, 58 deletions
diff --git a/query/lexer.go b/query/lexer.go index 5ca700c7..77830a47 100644 --- a/query/lexer.go +++ b/query/lexer.go @@ -11,16 +11,20 @@ type tokenKind int const ( _ tokenKind = iota tokenKindKV + tokenKindKVV tokenKindSearch ) type token struct { kind tokenKind - // KV + // KV and KVV qualifier string value string + // KVV only + subQualifier string + // Search term string } @@ -33,6 +37,15 @@ func newTokenKV(qualifier, value string) token { } } +func newTokenKVV(qualifier, subQualifier, value string) token { + return token{ + kind: tokenKindKVV, + qualifier: qualifier, + subQualifier: subQualifier, + value: value, + } +} + func newTokenSearch(term string) token { return token{ kind: tokenKindSearch, @@ -43,44 +56,68 @@ func newTokenSearch(term string) token { // 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, err := splitQuery(query) + fields, err := splitFunc(query, unicode.IsSpace) if err != nil { return nil, err } var tokens []token for _, field := range fields { - split := strings.Split(field, ":") - - // full text search - if len(split) == 1 { - tokens = append(tokens, newTokenSearch(removeQuote(field))) - continue + chunks, err := splitFunc(field, func(r rune) bool { return r == ':' }) + if err != nil { + return nil, err } - if len(split) != 2 { - return nil, fmt.Errorf("can't tokenize \"%s\"", field) + if strings.HasPrefix(field, ":") || strings.HasSuffix(field, ":") { + return nil, fmt.Errorf("empty qualifier or value") } - 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]) + // pre-process chunks + for i, chunk := range chunks { + if len(chunk) == 0 { + return nil, fmt.Errorf("empty qualifier or value") + } + chunks[i] = removeQuote(chunk) } - tokens = append(tokens, newTokenKV(split[0], removeQuote(split[1]))) + switch len(chunks) { + case 1: // full text search + tokens = append(tokens, newTokenSearch(chunks[0])) + + case 2: // KV + tokens = append(tokens, newTokenKV(chunks[0], chunks[1])) + + case 3: // KVV + tokens = append(tokens, newTokenKVV(chunks[0], chunks[1], chunks[2])) + + default: + return nil, fmt.Errorf("can't tokenize \"%s\": too many separators", field) + } } return tokens, nil } -// split the query into chunks by splitting on whitespaces but respecting +func removeQuote(field string) string { + runes := []rune(field) + if len(runes) >= 2 { + r1 := runes[0] + r2 := runes[len(runes)-1] + + if r1 == r2 && isQuote(r1) { + return string(runes[1 : len(runes)-1]) + } + } + return field +} + +// split the input into chunks by splitting according to separatorFunc but respecting // quotes -func splitQuery(query string) ([]string, error) { +func splitFunc(input string, separatorFunc func(r rune) bool) ([]string, error) { lastQuote := rune(0) inQuote := false - isToken := func(r rune) bool { + // return true if it's part of a chunk, or false if it's a rune that delimit one, as determined by the separatorFunc. + isChunk := func(r rune) bool { switch { case !inQuote && isQuote(r): lastQuote = r @@ -93,19 +130,19 @@ func splitQuery(query string) ([]string, error) { case inQuote: return true default: - return !unicode.IsSpace(r) + return !separatorFunc(r) } } var result []string - var token strings.Builder - for _, r := range query { - if isToken(r) { - token.WriteRune(r) + var chunk strings.Builder + for _, r := range input { + if isChunk(r) { + chunk.WriteRune(r) } else { - if token.Len() > 0 { - result = append(result, token.String()) - token.Reset() + if chunk.Len() > 0 { + result = append(result, chunk.String()) + chunk.Reset() } } } @@ -114,8 +151,8 @@ func splitQuery(query string) ([]string, error) { return nil, fmt.Errorf("unmatched quote") } - if token.Len() > 0 { - result = append(result, token.String()) + if chunk.Len() > 0 { + result = append(result, chunk.String()) } return result, nil @@ -124,16 +161,3 @@ func splitQuery(query string) ([]string, error) { func isQuote(r rune) bool { return r == '"' || r == '\'' } - -func removeQuote(field string) string { - runes := []rune(field) - if len(runes) >= 2 { - r1 := runes[0] - r2 := runes[len(runes)-1] - - if r1 == r2 && isQuote(r1) { - return string(runes[1 : len(runes)-1]) - } - } - return field -} diff --git a/query/lexer_test.go b/query/lexer_test.go index 59f17dec..6ef679d2 100644 --- a/query/lexer_test.go +++ b/query/lexer_test.go @@ -3,7 +3,7 @@ package query import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTokenize(t *testing.T) { @@ -37,6 +37,14 @@ func TestTokenize(t *testing.T) { {`key:'value value`, nil}, {`key:value value'`, nil}, + // sub-qualifier positive testing + {`key:subkey:"value:value"`, []token{newTokenKVV("key", "subkey", "value:value")}}, + + // sub-qualifier negative testing + {`key:subkey:value:value`, nil}, + {`key:subkey:`, nil}, + {`key:subkey:"value`, nil}, + // full text search {"search", []token{newTokenSearch("search")}}, {"search more terms", []token{ @@ -51,13 +59,15 @@ func TestTokenize(t *testing.T) { } 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) - } + t.Run(tc.input, func(t *testing.T) { + tokens, err := tokenize(tc.input) + if tc.tokens == nil { + require.Error(t, err) + require.Nil(t, tokens) + } else { + require.NoError(t, err) + require.Equal(t, tc.tokens, tokens) + } + }) } } diff --git a/query/parser.go b/query/parser.go index 762a47e5..6fd5cd74 100644 --- a/query/parser.go +++ b/query/parser.go @@ -67,6 +67,15 @@ func Parse(query string) (*Query, error) { default: return nil, fmt.Errorf("unknown qualifier \"%s\"", t.qualifier) } + + case tokenKindKVV: + switch t.qualifier { + case "metadata": + q.Metadata = append(q.Metadata, StringPair{Key: t.subQualifier, Value: t.value}) + + default: + return nil, fmt.Errorf("unknown qualifier \"%s:%s\"", t.qualifier, t.subQualifier) + } } } return q, nil diff --git a/query/parser_test.go b/query/parser_test.go index 87dd870a..cef01ffd 100644 --- a/query/parser_test.go +++ b/query/parser_test.go @@ -3,7 +3,7 @@ package query import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/bug" ) @@ -62,6 +62,11 @@ func TestParse(t *testing.T) { }}, {"sort:unknown", nil}, + // KVV + {`metadata:key:"https://www.example.com/"`, &Query{ + Filters: Filters{Metadata: []StringPair{{"key", "https://www.example.com/"}}}, + }}, + // Search {"search", &Query{ Search: []string{"search"}, @@ -90,17 +95,17 @@ func TestParse(t *testing.T) { 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) + require.Error(t, err) + require.Nil(t, query) } else { - assert.NoError(t, err) + require.NoError(t, err) if tc.output.OrderBy != 0 { - assert.Equal(t, tc.output.OrderBy, query.OrderBy) + require.Equal(t, tc.output.OrderBy, query.OrderBy) } if tc.output.OrderDirection != 0 { - assert.Equal(t, tc.output.OrderDirection, query.OrderDirection) + require.Equal(t, tc.output.OrderDirection, query.OrderDirection) } - assert.Equal(t, tc.output.Filters, query.Filters) + require.Equal(t, tc.output.Filters, query.Filters) } }) } diff --git a/query/query.go b/query/query.go index 816d6414..cce61a54 100644 --- a/query/query.go +++ b/query/query.go @@ -23,10 +23,17 @@ func NewQuery() *Query { type Search []string +// StringPair is a key/value pair of strings +type StringPair struct { + Key string + Value string +} + // Filters is a collection of Filter that implement a complex filter type Filters struct { Status []bug.Status Author []string + Metadata []StringPair Actor []string Participant []string Label []string |